#!/usr/bin/env elixir
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
#
# This is free public domain software for the public good of a permacomputer hosted
# at permacomputer.com - an always-on computer by the people, for the people. One
# which is durable, easy to repair, and distributed like tap water for machine
# learning intelligence.
#
# The permacomputer is community-owned infrastructure optimized around four values:
#
#   TRUTH    - First principles, math & science, open source code freely distributed
#   FREEDOM  - Voluntary partnerships, freedom from tyranny & corporate control
#   HARMONY  - Minimal waste, self-renewing systems with diverse thriving connections
#   LOVE     - Be yourself without hurting others, cooperation through natural law
#
# This software contributes to that vision by enabling code execution across 42+
# programming languages through a unified interface, accessible to all. Code is
# seeds to sprout on any abandoned technology.
#
# Learn more: https://www.permacomputer.com
#
# Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
# software, either in source code form or as a compiled binary, for any purpose,
# commercial or non-commercial, and by any means.
#
# NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
#
# That said, our permacomputer's digital membrane stratum continuously runs unit,
# integration, and functional tests on all of it's own software - with our
# permacomputer monitoring itself, repairing itself, with minimal human in the
# loop guidance. Our agents do their best.
#
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.timehexon.com
# https://www.foxhop.net
# https://www.unturf.com/software

# un.ex - Unsandbox CLI client in Elixir
#
# Full-featured CLI matching un.py capabilities:
# - Execute code with env vars, input files, artifacts
# - Interactive sessions with shell/REPL support
# - Persistent services with domains and ports
#
# Usage:
#   chmod +x un.ex
#   export UNSANDBOX_API_KEY="your_key_here"
#   ./un.ex [options] <source_file>
#   ./un.ex session [options]
#   ./un.ex service [options]
#
# Uses curl for HTTP (no external dependencies)

defmodule Un do
  @blue "\e[34m"
  @red "\e[31m"
  @green "\e[32m"
  @yellow "\e[33m"
  @reset "\e[0m"

  @portal_base "https://unsandbox.com"
  @languages_cache_ttl 3600

  @ext_map %{
    ".ex" => "elixir", ".exs" => "elixir", ".erl" => "erlang",
    ".py" => "python", ".js" => "javascript", ".ts" => "typescript",
    ".rb" => "ruby", ".go" => "go", ".rs" => "rust", ".c" => "c",
    ".cpp" => "cpp", ".cc" => "cpp", ".java" => "java", ".kt" => "kotlin",
    ".cs" => "csharp", ".fs" => "fsharp", ".hs" => "haskell",
    ".ml" => "ocaml", ".clj" => "clojure", ".scm" => "scheme",
    ".lisp" => "commonlisp", ".jl" => "julia", ".r" => "r",
    ".cr" => "crystal", ".d" => "d", ".nim" => "nim", ".zig" => "zig",
    ".v" => "v", ".dart" => "dart", ".groovy" => "groovy", ".scala" => "scala",
    ".sh" => "bash", ".pl" => "perl", ".lua" => "lua", ".php" => "php",
    ".f90" => "fortran", ".cob" => "cobol", ".pro" => "prolog",
    ".forth" => "forth", ".tcl" => "tcl", ".raku" => "raku"
  }

  def main([]), do: print_usage()
  def main(["session" | rest]), do: session_command(rest)
  def main(["service" | rest]), do: service_command(rest)
  def main(["snapshot" | rest]), do: snapshot_command(rest)
  def main(["image" | rest]), do: image_command(rest)
  def main(["key" | rest]), do: key_command(rest)
  def main(["languages" | rest]), do: languages_command(rest)
  def main(args), do: execute_command(args)

  defp print_usage do
    IO.puts("Usage: un.ex [options] <source_file>")
    IO.puts("       un.ex session [options]")
    IO.puts("       un.ex service [options]")
    IO.puts("       un.ex service env <action> <service_id>")
    IO.puts("       un.ex snapshot [options]")
    IO.puts("       un.ex image [options]")
    IO.puts("       un.ex key [--extend]")
    IO.puts("       un.ex languages [--json]")
    IO.puts("")
    IO.puts("Service options: --name, --ports, --bootstrap, -e KEY=VALUE, --env-file FILE")
    IO.puts("                --set-unfreeze-on-demand ID true|false")
    IO.puts("Service env commands: status, set, export, delete")
    IO.puts("Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,")
    IO.puts("               --publish ID --source-type TYPE, --visibility ID MODE,")
    IO.puts("               --spawn ID, --clone ID, --name NAME, --ports PORTS")
    IO.puts("Languages options: --json (output as JSON array)")
    System.halt(1)
  end

  # Execute command
  defp execute_command(args) do
    api_key = get_api_key()
    {file, opts} = parse_exec_args(args)

    if is_nil(file) do
      IO.puts(:stderr, "Error: No source file specified")
      System.halt(1)
    end

    ext = Path.extname(file)
    language = Map.get(@ext_map, ext)

    if is_nil(language) do
      IO.puts(:stderr, "Error: Unknown extension: #{ext}")
      System.halt(1)
    end

    case File.read(file) do
      {:ok, code} ->
        json = build_execute_json(language, code, opts)
        response = curl_post(api_key, "/execute", json)
        IO.puts(response)

      {:error, reason} ->
        IO.puts(:stderr, "Error reading file: #{reason}")
        System.halt(1)
    end
  end

  # Session command
  defp session_command(["--list" | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/sessions")
    IO.puts(response)
  end

  defp session_command(["--kill", session_id | _]) do
    api_key = get_api_key()
    curl_delete(api_key, "/sessions/#{session_id}")
    IO.puts("#{@green}Session terminated: #{session_id}#{@reset}")
  end

  defp session_command(["--snapshot", session_id | rest]) do
    api_key = get_api_key()
    name = get_opt(rest, "--snapshot-name", nil, nil)
    hot = "--hot" in rest
    name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
    hot_json = if hot, do: ",\"hot\":true", else: ""
    json = "{#{String.slice(name_json <> hot_json, 1..-1)}}"
    response = curl_post(api_key, "/sessions/#{session_id}/snapshot", json)
    IO.puts("#{@green}Snapshot created#{@reset}")
    IO.puts(response)
  end

  defp session_command(["--restore", snapshot_id | _rest]) do
    # --restore takes snapshot ID directly, calls /snapshots/:id/restore
    api_key = get_api_key()
    response = curl_post(api_key, "/snapshots/#{snapshot_id}/restore", "{}")
    IO.puts("#{@green}Session restored from snapshot#{@reset}")
    IO.puts(response)
  end

  defp session_command(args) do
    validate_session_args(args)
    api_key = get_api_key()
    shell = get_opt(args, "--shell", "-s", "bash")
    network = get_opt(args, "-n", nil, nil)
    vcpu = get_opt(args, "-v", nil, nil)
    input_files = get_all_opts(args, "-f")

    network_json = if network, do: ",\"network\":\"#{network}\"", else: ""
    vcpu_json = if vcpu, do: ",\"vcpu\":#{vcpu}", else: ""
    input_files_json = build_input_files_json(input_files)

    json = "{\"shell\":\"#{shell}\"#{network_json}#{vcpu_json}#{input_files_json}}"
    response = curl_post(api_key, "/sessions", json)
    IO.puts("#{@yellow}Session created (WebSocket required)#{@reset}")
    IO.puts(response)
  end

  defp validate_session_args([]), do: :ok
  defp validate_session_args(["--shell", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["-s", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["-f", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["-n", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["-v", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["--snapshot", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["--restore", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["--from", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["--snapshot-name", _ | rest]), do: validate_session_args(rest)
  defp validate_session_args(["--hot" | rest]), do: validate_session_args(rest)
  defp validate_session_args([arg | _]) do
    if String.starts_with?(arg, "-") do
      IO.puts(:stderr, "Unknown option: #{arg}")
      IO.puts(:stderr, "Usage: un.ex session [options]")
      System.halt(1)
    else
      validate_session_args([])
    end
  end

  # Service command
  defp service_command(["--list" | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/services")
    IO.puts(response)
  end

  defp service_command(["--info", service_id | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/services/#{service_id}")
    IO.puts(response)
  end

  defp service_command(["--logs", service_id | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/services/#{service_id}/logs")
    IO.puts(response)
  end

  defp service_command(["--freeze", service_id | _]) do
    api_key = get_api_key()
    curl_post(api_key, "/services/#{service_id}/freeze", "{}")
    IO.puts("#{@green}Service frozen: #{service_id}#{@reset}")
  end

  defp service_command(["--unfreeze", service_id | _]) do
    api_key = get_api_key()
    curl_post(api_key, "/services/#{service_id}/unfreeze", "{}")
    IO.puts("#{@green}Service unfreezing: #{service_id}#{@reset}")
  end

  defp service_command(["--destroy", service_id | _]) do
    api_key = get_api_key()
    curl_delete(api_key, "/services/#{service_id}")
    IO.puts("#{@green}Service destroyed: #{service_id}#{@reset}")
  end

  defp service_command(["--resize", service_id | rest]) do
    vcpu = get_opt(rest, "--vcpu", "-v", nil)

    if is_nil(vcpu) do
      IO.puts(:stderr, "#{@red}Error: --resize requires --vcpu N#{@reset}")
      System.halt(1)
    end

    vcpu_int = String.to_integer(vcpu)

    if vcpu_int < 1 or vcpu_int > 8 do
      IO.puts(:stderr, "#{@red}Error: --vcpu must be between 1 and 8#{@reset}")
      System.halt(1)
    end

    api_key = get_api_key()
    json = "{\"vcpu\":#{vcpu_int}}"
    curl_patch(api_key, "/services/#{service_id}", json)
    ram = vcpu_int * 2
    IO.puts("#{@green}Service resized to #{vcpu_int} vCPU, #{ram} GB RAM#{@reset}")
  end

  defp service_command(["--set-unfreeze-on-demand", service_id, enabled | _]) do
    api_key = get_api_key()
    enabled_bool = String.downcase(enabled) in ["true", "1", "yes", "on"]
    json = "{\"unfreeze_on_demand\":#{enabled_bool}}"
    curl_patch(api_key, "/services/#{service_id}", json)
    IO.puts("#{@green}Service unfreeze_on_demand set to #{enabled_bool}: #{service_id}#{@reset}")
  end

  defp service_command(["--snapshot", service_id | rest]) do
    api_key = get_api_key()
    name = get_opt(rest, "--snapshot-name", nil, nil)
    hot = "--hot" in rest
    name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
    hot_json = if hot, do: ",\"hot\":true", else: ""
    json = "{#{String.slice(name_json <> hot_json, 1..-1)}}"
    response = curl_post(api_key, "/services/#{service_id}/snapshot", json)
    IO.puts("#{@green}Snapshot created#{@reset}")
    IO.puts(response)
  end

  defp service_command(["--restore", snapshot_id | _rest]) do
    # --restore takes snapshot ID directly, calls /snapshots/:id/restore
    api_key = get_api_key()
    response = curl_post(api_key, "/snapshots/#{snapshot_id}/restore", "{}")
    IO.puts("#{@green}Service restored from snapshot#{@reset}")
    IO.puts(response)
  end

  defp service_command(["--execute", service_id, "--command", command | _]) do
    api_key = get_api_key()
    json = "{\"command\":\"#{escape_json(command)}\"}"
    response = curl_post(api_key, "/services/#{service_id}/execute", json)

    case extract_json_value(response, "stdout") do
      nil -> :ok
      stdout -> IO.write("#{@blue}#{stdout}#{@reset}")
    end
  end

  defp service_command(["--dump-bootstrap", service_id, file | _]) do
    api_key = get_api_key()
    IO.puts(:stderr, "Fetching bootstrap script from #{service_id}...")
    json = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
    response = curl_post(api_key, "/services/#{service_id}/execute", json)

    case extract_json_value(response, "stdout") do
      nil ->
        IO.puts(:stderr, "#{@red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{@reset}")
        System.halt(1)
      script ->
        File.write!(file, script)
        System.cmd("chmod", ["755", file])
        IO.puts("Bootstrap saved to #{file}")
    end
  end

  defp service_command(["--dump-bootstrap", service_id | _]) do
    api_key = get_api_key()
    IO.puts(:stderr, "Fetching bootstrap script from #{service_id}...")
    json = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
    response = curl_post(api_key, "/services/#{service_id}/execute", json)

    case extract_json_value(response, "stdout") do
      nil ->
        IO.puts(:stderr, "#{@red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{@reset}")
        System.halt(1)
      script ->
        IO.write(script)
    end
  end

  defp service_command(["env", "status", service_id | _]) do
    response = service_env_status(service_id)
    has_vault = extract_json_value(response, "has_vault") == "true"
    if has_vault do
      IO.puts("#{@green}Vault: configured#{@reset}")
      env_count = extract_json_value(response, "env_count")
      if env_count, do: IO.puts("Variables: #{env_count}")
      updated_at = extract_json_value(response, "updated_at")
      if updated_at, do: IO.puts("Updated: #{updated_at}")
    else
      IO.puts("#{@yellow}Vault: not configured#{@reset}")
    end
  end

  defp service_command(["env", "set", service_id | rest]) do
    envs = get_all_opts(rest, "-e")
    env_file = get_opt(rest, "--env-file", nil, nil)
    if Enum.empty?(envs) and is_nil(env_file) do
      IO.puts(:stderr, "#{@red}Error: service env set requires -e or --env-file#{@reset}")
      System.halt(1)
    end
    env_content = build_env_content(envs, env_file)
    if service_env_set(service_id, env_content) do
      IO.puts("#{@green}Vault updated for service #{service_id}#{@reset}")
    else
      IO.puts(:stderr, "#{@red}Error: Failed to update vault#{@reset}")
      System.halt(1)
    end
  end

  defp service_command(["env", "export", service_id | _]) do
    response = service_env_export(service_id)
    content = extract_json_value(response, "content")
    if content, do: IO.write(content)
  end

  defp service_command(["env", "delete", service_id | _]) do
    if service_env_delete(service_id) do
      IO.puts("#{@green}Vault deleted for service #{service_id}#{@reset}")
    else
      IO.puts(:stderr, "#{@red}Error: Failed to delete vault#{@reset}")
      System.halt(1)
    end
  end

  defp service_command(["env" | _]) do
    IO.puts(:stderr, "#{@red}Error: Usage: un.ex service env <status|set|export|delete> <service_id>#{@reset}")
    System.halt(1)
  end

  defp service_command(args) do
    name = get_opt(args, "--name", nil, nil)

    if is_nil(name) do
      IO.puts(:stderr, "Error: --name required to create service")
      System.halt(1)
    end

    api_key = get_api_key()
    ports = get_opt(args, "--ports", nil, nil)
    bootstrap = get_opt(args, "--bootstrap", nil, nil)
    bootstrap_file = get_opt(args, "--bootstrap-file", nil, nil)
    network = get_opt(args, "-n", nil, nil)
    vcpu = get_opt(args, "-v", nil, nil)
    service_type = get_opt(args, "--type", nil, nil)
    input_files = get_all_opts(args, "-f")
    envs = get_all_opts(args, "-e")
    env_file = get_opt(args, "--env-file", nil, nil)

    ports_json = if ports, do: ",\"ports\":[#{ports}]", else: ""
    bootstrap_json = if bootstrap, do: ",\"bootstrap\":\"#{escape_json(bootstrap)}\"", else: ""
    bootstrap_content_json = if bootstrap_file do
      case File.read(bootstrap_file) do
        {:ok, content} -> ",\"bootstrap_content\":\"#{escape_json(content)}\""
        {:error, _} ->
          IO.puts(:stderr, "#{@red}Error: Bootstrap file not found: #{bootstrap_file}#{@reset}")
          System.halt(1)
      end
    else
      ""
    end
    network_json = if network, do: ",\"network\":\"#{network}\"", else: ""
    vcpu_json = if vcpu, do: ",\"vcpu\":#{vcpu}", else: ""
    type_json = if service_type, do: ",\"service_type\":\"#{service_type}\"", else: ""
    input_files_json = build_input_files_json(input_files)

    json = "{\"name\":\"#{name}\"#{ports_json}#{bootstrap_json}#{bootstrap_content_json}#{network_json}#{vcpu_json}#{type_json}#{input_files_json}}"
    response = curl_post(api_key, "/services", json)
    IO.puts("#{@green}Service created#{@reset}")
    IO.puts(response)

    # Auto-set vault if env vars were provided
    service_id = extract_json_value(response, "id")
    if service_id and (not Enum.empty?(envs) or env_file) do
      env_content = build_env_content(envs, env_file)
      if String.length(env_content) > 0 do
        if service_env_set(service_id, env_content) do
          IO.puts("#{@green}Vault configured with environment variables#{@reset}")
        else
          IO.puts("#{@yellow}Warning: Failed to set vault#{@reset}")
        end
      end
    end
  end

  # Snapshot command
  defp snapshot_command(["--list" | _]) do
    snapshot_command(["-l"])
  end

  defp snapshot_command(["-l" | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/snapshots")
    IO.puts(response)
  end

  defp snapshot_command(["--info", snapshot_id | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/snapshots/#{snapshot_id}")
    IO.puts(response)
  end

  defp snapshot_command(["--delete", snapshot_id | _]) do
    api_key = get_api_key()
    curl_delete(api_key, "/snapshots/#{snapshot_id}")
    IO.puts("#{@green}Snapshot deleted: #{snapshot_id}#{@reset}")
  end

  defp snapshot_command(["--clone", snapshot_id | rest]) do
    api_key = get_api_key()
    clone_type = get_opt(rest, "--type", nil, nil)
    name = get_opt(rest, "--name", nil, nil)
    shell = get_opt(rest, "--shell", nil, nil)
    ports = get_opt(rest, "--ports", nil, nil)

    if !clone_type do
      IO.puts(:stderr, "#{@red}Error: --type required (session or service)#{@reset}")
      System.halt(1)
    end

    type_json = "\"type\":\"#{clone_type}\""
    name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
    shell_json = if shell, do: ",\"shell\":\"#{shell}\"", else: ""
    ports_json = if ports, do: ",\"ports\":[#{ports}]", else: ""
    json = "{#{type_json}#{name_json}#{shell_json}#{ports_json}}"

    response = curl_post(api_key, "/snapshots/#{snapshot_id}/clone", json)
    IO.puts("#{@green}Created from snapshot#{@reset}")
    IO.puts(response)
  end

  defp snapshot_command(_) do
    IO.puts(:stderr, "Error: Use --list, --info ID, --delete ID, or --clone ID --type TYPE")
    System.halt(1)
  end

  # Image command
  defp image_command(["--list" | _]) do
    image_command(["-l"])
  end

  defp image_command(["-l" | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/images")
    IO.puts(response)
  end

  defp image_command(["--info", image_id | _]) do
    api_key = get_api_key()
    response = curl_get(api_key, "/images/#{image_id}")
    IO.puts(response)
  end

  defp image_command(["--delete", image_id | _]) do
    api_key = get_api_key()
    curl_delete(api_key, "/images/#{image_id}")
    IO.puts("#{@green}Image deleted: #{image_id}#{@reset}")
  end

  defp image_command(["--lock", image_id | _]) do
    api_key = get_api_key()
    curl_post(api_key, "/images/#{image_id}/lock", "{}")
    IO.puts("#{@green}Image locked: #{image_id}#{@reset}")
  end

  defp image_command(["--unlock", image_id | _]) do
    api_key = get_api_key()
    curl_post(api_key, "/images/#{image_id}/unlock", "{}")
    IO.puts("#{@green}Image unlocked: #{image_id}#{@reset}")
  end

  defp image_command(["--publish", source_id | rest]) do
    source_type = get_opt(rest, "--source-type", nil, nil)
    if is_nil(source_type) do
      IO.puts(:stderr, "#{@red}Error: --source-type required (service or snapshot)#{@reset}")
      System.halt(1)
    end
    api_key = get_api_key()
    name = get_opt(rest, "--name", nil, nil)
    name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
    json = "{\"source_type\":\"#{source_type}\",\"source_id\":\"#{source_id}\"#{name_json}}"
    response = curl_post(api_key, "/images/publish", json)
    IO.puts("#{@green}Image published#{@reset}")
    IO.puts(response)
  end

  defp image_command(["--visibility", image_id, mode | _]) do
    api_key = get_api_key()
    json = "{\"visibility\":\"#{mode}\"}"
    curl_post(api_key, "/images/#{image_id}/visibility", json)
    IO.puts("#{@green}Image visibility set to #{mode}#{@reset}")
  end

  defp image_command(["--spawn", image_id | rest]) do
    api_key = get_api_key()
    name = get_opt(rest, "--name", nil, nil)
    ports = get_opt(rest, "--ports", nil, nil)
    name_json = if name, do: "\"name\":\"#{escape_json(name)}\"", else: ""
    ports_json = if ports, do: "\"ports\":[#{ports}]", else: ""
    parts = [name_json, ports_json] |> Enum.filter(&(&1 != "")) |> Enum.join(",")
    json = "{#{parts}}"
    response = curl_post(api_key, "/images/#{image_id}/spawn", json)
    IO.puts("#{@green}Service spawned from image#{@reset}")
    IO.puts(response)
  end

  defp image_command(["--clone", image_id | rest]) do
    api_key = get_api_key()
    name = get_opt(rest, "--name", nil, nil)
    json = if name, do: "{\"name\":\"#{escape_json(name)}\"}", else: "{}"
    response = curl_post(api_key, "/images/#{image_id}/clone", json)
    IO.puts("#{@green}Image cloned#{@reset}")
    IO.puts(response)
  end

  defp image_command(_) do
    IO.puts(:stderr, "Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID")
    System.halt(1)
  end

  # Languages cache functions
  defp get_languages_cache_path do
    home = System.get_env("HOME") || "."
    Path.join([home, ".unsandbox", "languages.json"])
  end

  defp load_languages_cache do
    cache_path = get_languages_cache_path()

    case File.read(cache_path) do
      {:ok, content} ->
        case Jason.decode(content) do
          {:ok, data} ->
            timestamp = Map.get(data, "timestamp", 0)
            now = System.system_time(:second)

            if now - timestamp < @languages_cache_ttl do
              Map.get(data, "languages", [])
            else
              nil
            end

          {:error, _} ->
            # Fallback to manual JSON parsing for environments without Jason
            timestamp = extract_json_number(content, "timestamp")
            now = System.system_time(:second)

            if timestamp != 0 and now - timestamp < @languages_cache_ttl do
              extract_json_array(content, "languages")
            else
              nil
            end
        end

      {:error, _} ->
        nil
    end
  rescue
    UndefinedFunctionError ->
      # Jason not available, use manual parsing
      cache_path = get_languages_cache_path()

      case File.read(cache_path) do
        {:ok, content} ->
          timestamp = extract_json_number(content, "timestamp")
          now = System.system_time(:second)

          if timestamp != 0 and now - timestamp < @languages_cache_ttl do
            extract_json_array(content, "languages")
          else
            nil
          end

        {:error, _} ->
          nil
      end
  end

  defp save_languages_cache(languages) do
    cache_path = get_languages_cache_path()
    cache_dir = Path.dirname(cache_path)

    # Ensure directory exists
    File.mkdir_p(cache_dir)

    timestamp = System.system_time(:second)
    languages_json = "[" <> Enum.map_join(languages, ",", &("\"#{&1}\"")) <> "]"
    json = "{\"languages\":#{languages_json},\"timestamp\":#{timestamp}}"

    File.write(cache_path, json)
  end

  defp extract_json_number(json_str, key) do
    case Regex.run(~r/"#{key}"\s*:\s*(\d+)/, json_str) do
      [_, value] -> String.to_integer(value)
      _ -> 0
    end
  end

  # Languages command
  defp languages_command(args) do
    json_output = "--json" in args

    # Try to load from cache first
    languages =
      case load_languages_cache() do
        nil ->
          # Cache miss or expired, fetch from API
          api_key = get_api_key()
          response = curl_get(api_key, "/languages")
          langs = extract_json_array(response, "languages")
          save_languages_cache(langs)
          langs

        cached_languages ->
          cached_languages
      end

    if json_output do
      # Output as JSON array
      json_str = "[" <> Enum.map_join(languages, ",", &("\"#{&1}\"")) <> "]"
      IO.puts(json_str)
    else
      # Output one language per line
      Enum.each(languages, &IO.puts/1)
    end
  end

  defp extract_json_array(json_str, key) do
    case Regex.run(~r/"#{key}"\s*:\s*\[([^\]]*)\]/, json_str) do
      [_, array_content] ->
        Regex.scan(~r/"([^"]*)"/, array_content)
        |> Enum.map(fn [_, val] -> val end)
      _ -> []
    end
  end

  # Key command
  defp key_command(args) do
    api_key = get_api_key()

    if "--extend" in args do
      validate_key(api_key, extend: true)
    else
      validate_key(api_key, extend: false)
    end
  end

  defp validate_key(api_key, extend: extend) do
    json = "{}"
    response = portal_curl_post(api_key, "/keys/validate", json)

    # Try to use Jason if available, otherwise fall back to manual parsing
    try do
      case Jason.decode(response) do
        {:ok, data} ->
          display_key_info(data, extend)

        {:error, _} ->
          # Fallback if Jason is not available, parse manually
          display_key_info_manual(response, extend)
      end
    rescue
      UndefinedFunctionError ->
        # If Jason module doesn't exist, use manual parsing
        display_key_info_manual(response, extend)
    end
  end

  defp display_key_info(data, extend) do
    status = Map.get(data, "status")
    public_key = Map.get(data, "public_key")
    tier = Map.get(data, "tier")
    expires_at = Map.get(data, "expires_at")
    time_remaining = Map.get(data, "time_remaining")
    rate_limit = Map.get(data, "rate_limit")
    burst = Map.get(data, "burst")
    concurrency = Map.get(data, "concurrency")

    case status do
      "valid" ->
        IO.puts("#{@green}Valid#{@reset}")
        IO.puts("Public Key: #{public_key}")
        IO.puts("Tier: #{tier}")
        IO.puts("Status: #{status}")
        IO.puts("Expires: #{expires_at}")
        if time_remaining, do: IO.puts("Time Remaining: #{time_remaining}")
        if rate_limit, do: IO.puts("Rate Limit: #{rate_limit}")
        if burst, do: IO.puts("Burst: #{burst}")
        if concurrency, do: IO.puts("Concurrency: #{concurrency}")

        if extend do
          open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
        end

      "expired" ->
        IO.puts("#{@red}Expired#{@reset}")
        IO.puts("Public Key: #{public_key}")
        IO.puts("Tier: #{tier}")
        IO.puts("Expired: #{expires_at}")
        IO.puts("#{@yellow}To renew: Visit #{@portal_base}/keys/extend#{@reset}")

        if extend do
          open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
        end

      "invalid" ->
        IO.puts("#{@red}Invalid#{@reset}")

      _ ->
        IO.puts("#{@red}Unknown status: #{status}#{@reset}")
    end
  end

  defp display_key_info_manual(response, extend) do
    # Simple manual parsing for JSON response
    status = extract_json_value(response, "status")
    public_key = extract_json_value(response, "public_key")
    tier = extract_json_value(response, "tier")
    expires_at = extract_json_value(response, "expires_at")
    time_remaining = extract_json_value(response, "time_remaining")
    rate_limit = extract_json_value(response, "rate_limit")
    burst = extract_json_value(response, "burst")
    concurrency = extract_json_value(response, "concurrency")

    case status do
      "valid" ->
        IO.puts("#{@green}Valid#{@reset}")
        IO.puts("Public Key: #{public_key}")
        IO.puts("Tier: #{tier}")
        IO.puts("Status: #{status}")
        IO.puts("Expires: #{expires_at}")
        if time_remaining, do: IO.puts("Time Remaining: #{time_remaining}")
        if rate_limit, do: IO.puts("Rate Limit: #{rate_limit}")
        if burst, do: IO.puts("Burst: #{burst}")
        if concurrency, do: IO.puts("Concurrency: #{concurrency}")

        if extend do
          open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
        end

      "expired" ->
        IO.puts("#{@red}Expired#{@reset}")
        IO.puts("Public Key: #{public_key}")
        IO.puts("Tier: #{tier}")
        IO.puts("Expired: #{expires_at}")
        IO.puts("#{@yellow}To renew: Visit #{@portal_base}/keys/extend#{@reset}")

        if extend do
          open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
        end

      "invalid" ->
        IO.puts("#{@red}Invalid#{@reset}")

      _ ->
        IO.puts("#{@red}Unknown status: #{status}#{@reset}")
        IO.puts(response)
    end
  end

  defp extract_json_value(json_str, key) do
    case Regex.run(~r/"#{key}"\s*:\s*"([^"]*)"/, json_str) do
      [_, value] -> value
      _ -> nil
    end
  end

  defp open_browser(url) do
    IO.puts("#{@blue}Opening browser: #{url}#{@reset}")

    case :os.type() do
      {:unix, :linux} ->
        System.cmd("xdg-open", [url], stderr_to_stdout: true)
      {:unix, :darwin} ->
        System.cmd("open", [url], stderr_to_stdout: true)
      {:win32, _} ->
        System.cmd("cmd", ["/c", "start", url], stderr_to_stdout: true)
      _ ->
        IO.puts("#{@yellow}Please open manually: #{url}#{@reset}")
    end
  end

  # Helpers
  defp get_api_keys do
    public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
    secret_key = System.get_env("UNSANDBOX_SECRET_KEY")

    # Fall back to UNSANDBOX_API_KEY for backwards compatibility
    api_key = System.get_env("UNSANDBOX_API_KEY")

    cond do
      public_key && secret_key ->
        {public_key, secret_key}
      api_key ->
        {api_key, nil}
      true ->
        IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")
        System.halt(1)
    end
  end

  defp get_api_key do
    {public_key, _} = get_api_keys()
    public_key
  end

  defp hmac_sha256(secret, message) do
    :crypto.mac(:hmac, :sha256, secret, message)
    |> Base.encode16(case: :lower)
  end

  defp make_signature(secret_key, timestamp, method, path, body) do
    message = "#{timestamp}:#{method}:#{path}:#{body}"
    hmac_sha256(secret_key, message)
  end

  defp escape_json(s) do
    s
    |> String.replace("\\", "\\\\")
    |> String.replace("\"", "\\\"")
    |> String.replace("\n", "\\n")
    |> String.replace("\r", "\\r")
    |> String.replace("\t", "\\t")
  end

  defp read_and_base64(filepath) do
    case File.read(filepath) do
      {:ok, content} -> Base.encode64(content)
      {:error, _} -> ""
    end
  end

  defp build_input_files_json([]), do: ""
  defp build_input_files_json(files) do
    file_jsons = files
      |> Enum.map(fn f ->
        b64 = read_and_base64(f)
        basename = Path.basename(f)
        "{\"filename\":\"#{escape_json(basename)}\",\"content\":\"#{b64}\"}"
      end)
      |> Enum.join(",")
    ",\"input_files\":[#{file_jsons}]"
  end

  defp build_execute_json(language, code, _opts) do
    "{\"language\":\"#{language}\",\"code\":\"#{escape_json(code)}\"}"
  end

  defp curl_post(api_key, endpoint, json) do
    tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
    File.write!(tmp_file, json)

    {public_key, secret_key} = get_api_keys()
    headers = build_auth_headers(public_key, secret_key, "POST", endpoint, json)

    args = [
      "-s", "-X", "POST",
      "https://api.unsandbox.com#{endpoint}",
      "-H", "Content-Type: application/json"
    ] ++ headers ++ ["-d", "@#{tmp_file}"]

    {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)

    File.rm(tmp_file)
    check_clock_drift(output)
    output
  end

  defp build_auth_headers(public_key, secret_key, method, path, body) do
    if secret_key do
      timestamp = System.system_time(:second) |> Integer.to_string()
      signature = make_signature(secret_key, timestamp, method, path, body)
      [
        "-H", "Authorization: Bearer #{public_key}",
        "-H", "X-Timestamp: #{timestamp}",
        "-H", "X-Signature: #{signature}"
      ]
    else
      # Backwards compatibility: use simple bearer token
      ["-H", "Authorization: Bearer #{public_key}"]
    end
  end

  defp portal_curl_post(api_key, endpoint, json) do
    tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
    File.write!(tmp_file, json)

    {public_key, secret_key} = get_api_keys()
    headers = build_auth_headers(public_key, secret_key, "POST", endpoint, json)

    args = [
      "-s", "-X", "POST",
      "#{@portal_base}#{endpoint}",
      "-H", "Content-Type: application/json"
    ] ++ headers ++ ["-d", "@#{tmp_file}"]

    {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)

    File.rm(tmp_file)
    check_clock_drift(output)
    output
  end

  defp curl_get(api_key, endpoint) do
    {public_key, secret_key} = get_api_keys()
    headers = build_auth_headers(public_key, secret_key, "GET", endpoint, "")

    args = [
      "-s",
      "https://api.unsandbox.com#{endpoint}"
    ] ++ headers

    {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)

    check_clock_drift(output)
    output
  end

  defp curl_delete(api_key, endpoint) do
    {public_key, secret_key} = get_api_keys()
    headers = build_auth_headers(public_key, secret_key, "DELETE", endpoint, "")

    args = [
      "-s", "-X", "DELETE",
      "https://api.unsandbox.com#{endpoint}"
    ] ++ headers

    {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)

    check_clock_drift(output)
    output
  end

  defp curl_patch(api_key, endpoint, json) do
    tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
    File.write!(tmp_file, json)

    {public_key, secret_key} = get_api_keys()
    headers = build_auth_headers(public_key, secret_key, "PATCH", endpoint, json)

    args = [
      "-s", "-X", "PATCH",
      "https://api.unsandbox.com#{endpoint}",
      "-H", "Content-Type: application/json"
    ] ++ headers ++ ["-d", "@#{tmp_file}"]

    {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)

    File.rm(tmp_file)
    check_clock_drift(output)
    output
  end

  defp curl_put_text(endpoint, body) do
    tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.txt"
    File.write!(tmp_file, body)

    {public_key, secret_key} = get_api_keys()
    headers = build_auth_headers(public_key, secret_key, "PUT", endpoint, body)

    args = [
      "-s", "-o", "/dev/null", "-w", "%{http_code}",
      "-X", "PUT",
      "https://api.unsandbox.com#{endpoint}",
      "-H", "Content-Type: text/plain"
    ] ++ headers ++ ["-d", "@#{tmp_file}"]

    {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)

    File.rm(tmp_file)
    status_code = String.trim(output) |> String.to_integer()
    status_code >= 200 and status_code < 300
  end

  @max_env_content_size 65536

  defp read_env_file(path) do
    case File.read(path) do
      {:ok, content} -> content
      {:error, _} ->
        IO.puts(:stderr, "#{@red}Error: Env file not found: #{path}#{@reset}")
        System.halt(1)
    end
  end

  defp build_env_content(envs, env_file) do
    file_lines = if env_file do
      content = read_env_file(env_file)
      content
      |> String.split("\n")
      |> Enum.map(&String.trim/1)
      |> Enum.filter(fn line ->
        String.length(line) > 0 and not String.starts_with?(line, "#")
      end)
    else
      []
    end
    (envs ++ file_lines) |> Enum.join("\n")
  end

  defp service_env_status(service_id) do
    api_key = get_api_key()
    curl_get(api_key, "/services/#{service_id}/env")
  end

  defp service_env_set(service_id, env_content) do
    if String.length(env_content) > @max_env_content_size do
      IO.puts(:stderr, "#{@red}Error: Env content exceeds maximum size of 64KB#{@reset}")
      false
    else
      curl_put_text("/services/#{service_id}/env", env_content)
    end
  end

  defp service_env_export(service_id) do
    api_key = get_api_key()
    curl_post(api_key, "/services/#{service_id}/env/export", "{}")
  end

  defp service_env_delete(service_id) do
    api_key = get_api_key()
    curl_delete(api_key, "/services/#{service_id}/env")
    true
  end

  defp parse_exec_args(args) do
    parse_exec_args(args, nil, %{})
  end

  defp parse_exec_args([], file, opts), do: {file, opts}

  defp parse_exec_args([arg | rest], file, opts) do
    cond do
      String.starts_with?(arg, "-") ->
        parse_exec_args(rest, file, opts)
      is_nil(file) ->
        parse_exec_args(rest, arg, opts)
      true ->
        parse_exec_args(rest, file, opts)
    end
  end

  defp get_opt([], _long, _short, default), do: default

  defp get_opt([arg, value | rest], long, short, _default) when arg == long or arg == short do
    value
  end

  defp get_opt([_arg | rest], long, short, default) do
    get_opt(rest, long, short, default)
  end

  defp get_all_opts(args, flag), do: get_all_opts(args, flag, [])

  defp get_all_opts([], _flag, acc), do: Enum.reverse(acc)

  defp get_all_opts([arg, value | rest], flag, acc) when arg == flag do
    get_all_opts(rest, flag, [value | acc])
  end

  defp get_all_opts([_arg | rest], flag, acc) do
    get_all_opts(rest, flag, acc)
  end

  defp check_clock_drift(response) do
    response_lower = String.downcase(response)

    # Check if response contains "timestamp" and error indicators
    has_timestamp = String.contains?(response_lower, "timestamp")
    has_error = String.contains?(response_lower, "401") or
                String.contains?(response_lower, "expired") or
                String.contains?(response_lower, "invalid")

    if has_timestamp and has_error do
      IO.puts(:stderr, "#{@red}Error: Request timestamp expired (must be within 5 minutes of server time)#{@reset}")
      IO.puts(:stderr, "#{@yellow}Your computer's clock may have drifted.")
      IO.puts(:stderr, "Check your system time and sync with NTP if needed:")
      IO.puts(:stderr, "  Linux:   sudo ntpdate -s time.nist.gov")
      IO.puts(:stderr, "  macOS:   sudo sntp -sS time.apple.com")
      IO.puts(:stderr, "  Windows: w32tm /resync#{@reset}")
      System.halt(1)
    end
  end
end

Un.main(System.argv())
