defmodule Services.Approvals.Shell do
  # ----------------------------------------------------------------------------
  # Globals
  # ----------------------------------------------------------------------------
  @type state :: Services.Approvals.Workflow.state()
  @type decision :: Services.Approvals.Workflow.decision()
  @type args :: {String.t(), list(map), String.t()}

  @approve "Approve"
  @persistent "Approve persistently"
  @deny "Deny"
  @deny_feedback "Deny with feedback"

  @session "Approve for this session"
  @project "Approve for the project"
  @global "Approve globally"

  @no_feedback "The user denied the request."

  @no_tty """
  The application is not running in an interactive terminal.
  The user cannot respond to prompts, so they were unable to approve or deny the request.
  """

  # ----------------------------------------------------------------------------
  # Behaviour implementation
  # ----------------------------------------------------------------------------
  @behaviour Services.Approvals.Workflow

  @impl Services.Approvals.Workflow
  @spec confirm(state, args) :: decision
  def confirm(state, {op, commands, purpose}) when is_list(commands) do
    with :ok <- validate_commands(commands) do
      # Build list of {prefix, full_command} pairs
      stages =
        Enum.map(commands, fn cmd ->
          {
            extract_prefix(cmd),
            format_stage_for_match(cmd)
          }
        end)

      if Enum.all?(stages, &approved?(state, &1)) do
        {:approved, state}
      else
        if !UI.is_tty?() do
          UI.error("Shell", @no_tty)
          {:denied, @no_tty, state}
        else
          UI.interact(fn ->
            render_pipeline(state, op, commands, purpose)
            prompt(state, stages)
          end)
        end
      end
    else
      {:error, reason} -> {:denied, reason, state}
    end
  end

  # Input validation
  # ----------------------------------------------------------------------------
  defp validate_commands([]), do: :ok

  defp validate_commands([cmd | rest]) do
    with :ok <- validate_command(cmd),
         :ok <- validate_commands(rest) do
      :ok
    end
  end

  defp validate_command(%{"command" => cmd, "args" => args}) do
    cond do
      !is_binary(cmd) -> {:error, "command must be a string"}
      !is_list(args) -> {:error, "args must be a list"}
      !Enum.all?(args, &is_binary/1) -> {:error, "all args must be strings"}
      true -> :ok
    end
  end

  defp validate_command(_), do: {:error, "expected object with keys 'command' and 'args'"}

  # ----------------------------------------------------------------------------
  # Approval checks
  # ----------------------------------------------------------------------------
  # Pre-approved regex for read-only sed commands commonly used by LLMs
  # Supports: -n (quiet), -E (extended regex), basic s/// substitutions, p (print), d (delete without -i)
  # Blocks: -i (in-place), -f (script files), -e (expressions), s///e (execute), w/W/r file operations
  # no in-place editing
  # no script files
  # no -e expressions (conservative)
  # no s///e or s|||e execute in substitution
  # no w/W/r commands with filenames
  # no addressed w/W/r commands
  # no range w/W/r commands
  @sed_readonly_pattern "^sed" <>
                          "(?!.*\\s-i\\b)" <>
                          "(?!.*\\s-f\\b)" <>
                          "(?!.*\\s-e\\b)" <>
                          "(?!.*s[|/].*[|/].*[|/][gp0-9]*e)" <>
                          "(?!.*\\b[wWr]\\s+\\S)" <>
                          "(?!.*\\d+[wWr]\\b)" <>
                          "(?!.*,[wWr]\\b)" <>
                          ".+$"

  @full_cmd [
    "^find(?!.*-exec)",
    @sed_readonly_pattern
  ]

  @ro_cmd [
    "ag",
    "cat",
    "col",
    "cut",
    "diff",
    "echo",
    "fgrep",
    "grep",
    "head",
    "jq",
    "ls",
    "nl",
    "pwd",
    "rg",
    "sort",
    "tac",
    "tail",
    "tr",
    "tree",
    "uniq",
    "wc",

    # git subcommands
    "git blame",
    "git branch",
    "git describe",
    "git diff",
    "git grep",
    "git log",
    "git ls-files",
    "git merge-base",
    "git rev-list",
    "git rev-parse",
    "git show",
    "git status"
  ]

  @rw_cmd [
    "mkdir",
    "touch",
    "cp",
    "mv",
    "rm",
    "sed",
    "awk",
    "patch",
    "truncate"
  ]

  def preapproved_cmds do
    if edit?() and auto?() do
      @ro_cmd ++ @rw_cmd
    else
      @ro_cmd
    end
  end

  # Returns true if either the prefix path or the full command string is
  # approved or a stored full-command approval matches this stage.
  defp approved?(state, {prefix, full}) do
    prefix_approved?(state, prefix) or
      full_cmd_preapproved?(state, full)
  end

  defp session_approvals(%{session: session}, kind) do
    session
    |> Enum.reduce([], fn
      {^kind, prefix}, acc -> [prefix | acc]
      _, acc -> acc
    end)
  end

  def prefix_approved?(state, prefix) do
    cond do
      prefix in preapproved_cmds() -> true
      prefix in session_approvals(state, :prefix) -> true
      Settings.Approvals.prefix_approved?(Settings.new(), "shell", prefix) -> true
      true -> false
    end
  end

  def full_cmd_preapproved?(state, full) do
    Settings.Approvals.approved?(Settings.new(), "shell_full", full) or
      @full_cmd
      |> Enum.concat(session_approvals(state, :full))
      |> Enum.map(fn re -> Regex.compile!(re, "u") end)
      |> Enum.any?(&Regex.match?(&1, full))
  end

  # ----------------------------------------------------------------------------
  # Display
  # ----------------------------------------------------------------------------
  defp render_pipeline(state, op, commands, purpose) do
    stages =
      commands
      |> Enum.with_index(1)
      |> Enum.flat_map(fn {cmd, i} ->
        tags =
          if approved?(state, {extract_prefix(cmd), format_stage_for_match(cmd)}) do
            [:green, :bright]
          else
            [:red, :bright]
          end

        formatted = format_stage(cmd)
        item = "#{i}. #{formatted}"

        [Owl.Data.tag(item, tags), "\n\n"]
      end)

    op_msg =
      case op do
        "|" -> "Pipeline of Commands"
        "&&" -> "Chained Commands"
      end

    [
      Owl.Data.tag("# Approval Scope ", [:red_background, :black, :bright]),
      "\n\n#{op_msg}:\n\n"
    ]
    |> Enum.concat(stages)
    |> Enum.concat([
      Owl.Data.tag("# Purpose ", [:red_background, :black, :bright]),
      "\n\n",
      purpose
    ])
    |> UI.box(
      title: " Shell ",
      min_width: 80,
      padding: 1,
      horizontal_align: :left,
      border_tag: [:red, :bright]
    )
  end

  defp format_stage(%{"command" => cmd, "args" => args}) do
    Enum.join([cmd | args], " ")
  end

  # Build the string used for shell_full matching: use basename(command) + args
  defp format_stage_for_match(%{"command" => cmd, "args" => args}) do
    base = Path.basename(cmd)
    Enum.join([base | args], " ")
  end

  # ----------------------------------------------------------------------------
  # Prompt + persistence
  # ----------------------------------------------------------------------------
  defp prompt(state, stages) do
    opts = [@approve, @persistent, @deny, @deny_feedback]

    Settings.get_auto_policy()
    |> case do
      {:approve, ms} -> UI.choose("Approve this request?", opts, ms, @approve)
      {:deny, ms} -> UI.choose("Approve this request?", opts, ms, @deny)
      _ -> UI.choose("Approve this request?", opts)
    end
    |> case do
      @approve ->
        {:approved, state}

      @deny ->
        {:denied, build_auto_deny_message(), state}

      @deny_feedback ->
        {:denied, get_feedback(), state}

      @persistent ->
        customize(state, stages)
    end
  end

  defp unapproved_prefixes(state, stages) do
    stages
    |> Enum.uniq()
    |> Enum.reject(fn {prefix, full} -> approved?(state, {prefix, full}) end)
    |> Enum.map(fn {prefix, _full} -> prefix end)
    |> Enum.uniq()
  end

  @spec customize(state, list({String.t(), String.t()})) :: {:approved, state}
  def customize(state, stages) do
    unapproved_prefixes(state, stages)
    |> Enum.reduce({:approved, state}, fn prefix, {:approved, acc_state} ->
      choose_scope(acc_state, prefix)
    end)
  end

  defp choose_scope(state, prefix) do
    {kind, pattern} = custom_pattern(state, prefix)

    scope =
      UI.choose(
        """
        Choose approval scope for:
            #{pattern}
        """,
        [@session, @project, @global]
      )

    case kind do
      :prefix -> approve_scope(scope, state, pattern)
      :full -> approve_regex_scope(scope, state, pattern, prefix)
    end
  end

  defp custom_pattern(state, prefix) do
    UI.prompt(
      """
      You may optionally select your own approval prefix, making it broader or more specific.

      Wrap your input in slashes (/) to treat it as a regular expression.
      Regular expressions are matched against the entire command string, including arguments.
      There is NO implicit anchoring, so include ^ and $ as needed.

      Options:
      1. Customize the prefix (e.g. `docker image` to `docker image ls` or `docker` to change the specificity)
      2. Enter a regular expression (e.g. `/^find(?!.*-exec).+$/` to allow find without -exec)
      3. *Leave blank* to approve the default prefix as shown.

      CURRENT PREFIX: #{prefix}
      """,
      optional: true
    )
    |> case do
      {:error, :no_tty} ->
        {:prefix, prefix}

      nil ->
        {:prefix, prefix}

      "" ->
        {:prefix, prefix}

      str ->
        str = String.trim(str)

        if String.starts_with?(str, "/") and String.ends_with?(str, "/") do
          re = String.slice(str, 1..-2//1)

          if re == "" do
            UI.error("Empty regex is not allowed")
            custom_pattern(state, prefix)
          else
            case Regex.compile(re, "u") do
              {:ok, _} ->
                {:full, re}

              {:error, {msg, _pos}} ->
                UI.error("Invalid regex: #{to_string(msg)}")
                custom_pattern(state, prefix)
            end
          end
        else
          {:prefix, str}
        end
    end
  end

  defp approve_scope(@session, %{session: session} = state, prefix) do
    prefixes = session |> Enum.concat([{:prefix, prefix}]) |> Enum.uniq()
    {:approved, %{state | session: prefixes}}
  end

  defp approve_scope(@project, state, prefix) do
    Settings.new() |> Settings.Approvals.approve(:project, "shell", prefix)
    {:approved, state}
  end

  defp approve_scope(@global, state, prefix) do
    Settings.new() |> Settings.Approvals.approve(:global, "shell", prefix)
    {:approved, state}
  end

  defp approve_regex_scope(@session, %{session: session} = state, inner, _prefix) do
    prefixes = session |> Enum.concat([{:full, inner}]) |> Enum.uniq()
    {:approved, %{state | session: prefixes}}
  end

  defp approve_regex_scope(@project, state, inner, _prefix) do
    Settings.new() |> Settings.Approvals.approve(:project, "shell_full", inner)
    {:approved, state}
  end

  defp approve_regex_scope(@global, state, inner, _prefix) do
    Settings.new() |> Settings.Approvals.approve(:global, "shell_full", inner)
    {:approved, state}
  end

  # ----------------------------------------------------------------------------
  # Utilities
  # ----------------------------------------------------------------------------

  @doc """
  Delegate to the pure prefix extraction logic.
  """
  def extract_prefix(%{"command" => cmd, "args" => args}) do
    base_cmd = Path.basename(cmd)
    Services.Approvals.Shell.Prefix.extract(base_cmd, args)
  end

  # Prompt user for feedback when denying with feedback
  defp get_feedback do
    "Feedback:"
    |> UI.prompt()
    |> then(&"The user denied the request with the following feedback: #{&1}")
  end

  defp build_auto_deny_message() do
    case Settings.get_auto_policy() do
      {:deny, ms} when is_integer(ms) and ms > 0 ->
        seconds = div(ms, 1000)

        """
        The request was automatically denied after #{seconds} seconds due to an active auto-deny policy.
        The user may not be monitoring their terminal. Only use pre-approved shell commands that will not require user confirmation.
        """
        |> String.trim()

      _ ->
        @no_feedback
    end
  end

  defp edit?, do: Settings.get_edit_mode()
  defp auto?, do: edit?() && Settings.get_auto_approve()
end
