;;; ai-code-backends.el --- Backend selection support for ai-code -*- lexical-binding: t; -*-

;; Author: Kang Tu <tninja@gmail.com>

;; SPDX-License-Identifier: Apache-2.0

;;; Commentary:
;; Backend selection support extracted from ai-code.el.

;;; Code:

(require 'seq)

(require 'ai-code-git)

(defvar ai-code-cli)
(defvar claude-code-terminal-backend)
(defvar helm-completion-styles-alist)
(defvar ivy-sort-functions-alist)
(defvar vertico-sort-override-function)

(eval-when-compile
  (defvar ai-code-selected-backend))

(declare-function claude-code--do-send-command "claude-code" (cmd))
(declare-function claude-code--term-send-string "claude-code" (backend string))
(declare-function ai-code--validate-git-repository "ai-code-git" ())
(declare-function ai-code--git-root "ai-code-utils" (&optional dir))
(declare-function ai-code-onboarding-show-backend-switch-hint "ai-code-onboarding" ())
(declare-function ai-code-read-string "ai-code-input" (prompt &optional initial-input candidate-list))

(defvar ai-code--cli-start-fn #'ai-code--unsupported-start)
(defvar ai-code--cli-resume-fn #'ai-code--unsupported-resume)
(defvar ai-code--cli-switch-fn #'ai-code--unsupported-switch-to-buffer)
(defvar ai-code--cli-send-fn #'ai-code--unsupported-send-command)
(defvar ai-code-selected-backend 'claude-code
  "Currently selected backend key from `ai-code-backends'.")

(defvar ai-code--repo-backend-alist nil
  "Alist of (GIT-ROOT . BACKEND) to keep backend affinity per repository.")

(defun ai-code--normalize-git-root (git-root)
  "Return normalized GIT-ROOT path, or nil when invalid."
  (when (and (stringp git-root) (> (length git-root) 0))
    (file-truename git-root)))

(defun ai-code--current-git-root ()
  "Return normalized current git root for backend affinity, or nil."
  (let ((git-root
         (cond
          ((fboundp 'ai-code--git-root)
           (ai-code--git-root))
          ((fboundp 'magit-toplevel)
           (condition-case nil
               (magit-toplevel)
             (error nil)))
          (t nil))))
    (ai-code--normalize-git-root git-root)))

(defun ai-code--repo-backend-for-root (git-root)
  "Return remembered backend for GIT-ROOT, or nil."
  (cdr (assoc git-root ai-code--repo-backend-alist)))

(defun ai-code--remember-repo-backend (git-root backend)
  "Remember BACKEND for GIT-ROOT."
  (when (and git-root backend)
    (setq ai-code--repo-backend-alist
          (cons (cons git-root backend)
                (seq-remove (lambda (it)
                              (string= (car it) git-root))
                            ai-code--repo-backend-alist)))))

(defun ai-code--effective-backend ()
  "Return backend for current context, preferring repo-local affinity."
  (let* ((git-root (ai-code--current-git-root))
         (repo-backend (and git-root (ai-code--repo-backend-for-root git-root))))
    (if (and repo-backend (ai-code--backend-spec repo-backend))
        repo-backend
      ai-code-selected-backend)))

(defun ai-code--activate-effective-backend ()
  "Switch active backend to the effective backend for current context."
  (let ((effective (ai-code--effective-backend)))
    (when (and effective (not (eq effective ai-code-selected-backend)))
      (ai-code-set-backend effective))))

(defun ai-code--remember-current-backend-for-repo ()
  "Remember current backend for current git repository."
  (let ((git-root (ai-code--current-git-root)))
    (when git-root
      (ai-code--remember-repo-backend git-root ai-code-selected-backend))))

(defun ai-code--unsupported-start (&optional _arg)
  "Signal that the current backend does not support start.
Argument _ARG is ignored."
  (interactive "P")
  (user-error "Backend '%s' does not support start"
              (ai-code-current-backend-label)))

(defun ai-code--unsupported-switch-to-buffer (&optional _arg)
  "Signal that the current backend does not support switching.
Argument _ARG is ignored."
  (interactive "P")
  (user-error "Backend '%s' does not support switching buffers"
              (ai-code-current-backend-label)))

(defun ai-code--unsupported-send-command (&optional _command)
  "Signal that the current backend does not support sending commands.
Argument _COMMAND is ignored."
  (interactive)
  (user-error "Backend '%s' does not support sending commands"
              (ai-code-current-backend-label)))

(defun ai-code--unsupported-resume (&optional _arg)
  "Signal that the current backend does not support resume.
Argument _ARG is ignored."
  (interactive "P")
  (user-error "Backend '%s' does not support resume"
              (ai-code-current-backend-label)))

;;;###autoload
(defun ai-code-cli-start (&optional arg)
  "Start the current backend's CLI session when supported.
Argument ARG is passed to the backend's start function.
When called interactively, any prefix argument is forwarded via
`current-prefix-arg', and it is up to the backend how to interpret
it (for example, native CLI backends may use a non-nil prefix to
prompt for CLI arguments and a working directory)."
  (interactive "P")
  (ai-code--activate-effective-backend)
  (prog1
      (if (called-interactively-p 'interactive)
          (call-interactively ai-code--cli-start-fn)
        (if arg
            (funcall ai-code--cli-start-fn arg)
          (funcall ai-code--cli-start-fn)))
    (ai-code--remember-current-backend-for-repo)))

;;;###autoload
(defun ai-code-cli-resume (&optional arg)
  "Resume the current backend's CLI session when supported.
Noninteractive callers pass ARG to the backend resume function.
When called interactively, any prefix argument is forwarded via
`current-prefix-arg', and it is up to the backend how to interpret
it (for example, some backends may use a non-nil prefix to prompt for
additional CLI arguments and a working directory)."
  (interactive "P")
  (ai-code--activate-effective-backend)
  (prog1
      (if (called-interactively-p 'interactive)
          (call-interactively ai-code--cli-resume-fn)
        (if arg
            (funcall ai-code--cli-resume-fn arg)
          (funcall ai-code--cli-resume-fn)))
    (ai-code--remember-current-backend-for-repo)))

;;;###autoload
(defun ai-code-cli-switch-to-buffer (&optional arg)
  "Switch to the current backend's CLI buffer when supported.
Argument ARG is passed to the backend's switch function."
  (interactive "P")
  (ai-code--activate-effective-backend)
  (if (called-interactively-p 'interactive)
      (call-interactively ai-code--cli-switch-fn)
    (if arg
        (funcall ai-code--cli-switch-fn arg)
      (funcall ai-code--cli-switch-fn))))

(defun ai-code--missing-session-error-p (error-data)
  "Return non-nil when ERROR-DATA reports a missing AI session."
  (string-match-p
   "\\(?:\\`\\|: \\)No [^\n]+ session\\(?: for this project\\|;\\)"
   (error-message-string error-data)))

(defun ai-code--send-command-with-session-recovery (command)
  "Send COMMAND, offering to start a missing session before one retry."
  (let ((source-buffer (current-buffer)))
    (condition-case err
        (progn
          (funcall ai-code--cli-send-fn command)
          t)
      (error
       (if (not (ai-code--missing-session-error-p err))
           (signal (car err) (cdr err))
         (if (not (y-or-n-p
                   (format "No %s session for this project.  Start one? "
                           (ai-code-current-backend-label))))
             nil
           (ai-code-cli-start)
           (if (not (y-or-n-p "Ready to send prompt? "))
               nil
             (if (buffer-live-p source-buffer)
                 (with-current-buffer source-buffer
                   (funcall ai-code--cli-send-fn command))
               (funcall ai-code--cli-send-fn command))
             t)))))))

;;;###autoload
(defun ai-code-cli-send-command (&optional command)
  "Send COMMAND to the current backend when supported.
When called interactively, prompt for COMMAND.
Noninteractive callers must supply COMMAND."
  (interactive)
  (ai-code--activate-effective-backend)
  (if (called-interactively-p 'interactive)
      (call-interactively ai-code--cli-send-fn)
    (if (null command)
        (user-error "COMMAND is required for noninteractive calls")
      (ai-code--send-command-with-session-recovery command))))

;;;###autoload
(defun ai-code-claude-code-el-send-command (cmd)
  "Send CMD to claude-code programmatically or interactively.
This wrapper function works around the signature change in
`claude-code-send-command' which no longer accepts a command parameter.
When called interactively, prompts for the command.
When called from Lisp code, sends CMD directly without prompting."
  (interactive "sClaude command: ")
  (claude-code--do-send-command cmd))

(defun ai-code-claude-code-install-skills ()
  "Install skills for Claude Code by prompting for a skills repo URL.
Ask the Claude Code CLI to clone and set up the skills from the given
repository.  Claude Code manages skills as files under ~/.claude/,
so the CLI itself handles the installation details."
  (let* ((url (read-string
               "Skills repo URL for Claude Code: "
               nil nil "https://github.com/obra/superpowers"))
         (default-prompt
          (format
           "Install the skill from %s for this Claude Code CLI. Read the repository README to understand the installation instructions and follow them. Set up the skill files under the appropriate directory (e.g. ~/.claude/ or the project .claude/ directory) so they are available in future sessions."
           url))
         (prompt (if (called-interactively-p 'interactive)
                     (ai-code-read-string
                      "Edit install-skills prompt for Claude Code: "
                      default-prompt)
                   default-prompt)))
    (ai-code-cli-send-command prompt)))

;;;###autoload
(defcustom ai-code-backends
  '((claude-code
     :label "Claude Code"
     :require ai-code-claude-code
     :start   ai-code-claude-code
     :switch  ai-code-claude-code-switch-to-buffer
     :send    ai-code-claude-code-send-command
     :resume  ai-code-claude-code-resume
     :config  "~/.claude.json"
     :agent-file "CLAUDE.md"
     :install "npm install -g @anthropic-ai/claude-code@latest"
     :upgrade nil
     :install-skills nil
     :cli     "claude")
    (gemini
     :label "Gemini CLI"
     :require ai-code-gemini-cli
     :start   ai-code-gemini-cli
     :switch  ai-code-gemini-cli-switch-to-buffer
     :send    ai-code-gemini-cli-send-command
     :resume  ai-code-gemini-cli-resume
     :config  "~/.gemini/settings.json"
     :agent-file "GEMINI.md"
     :install "npm install -g @google/gemini-cli"
     :upgrade nil
     :install-skills nil
     :cli     "gemini")
    (antigravity
     :label "Antigravity CLI"
     :require ai-code-antigravity-cli
     :start    ai-code-antigravity-cli
     :switch  ai-code-antigravity-cli-switch-to-buffer
     :send    ai-code-antigravity-cli-send-command
     :resume  ai-code-antigravity-cli-resume
     :config  "~/.gemini/antigravity-cli/settings.json"
     :agent-file "AGENTS.md"
     :install "curl -fsSL https://antigravity.google/cli/install.sh | bash"
     :upgrade "agy update"
     :install-skills nil
     :cli     "agy")
    (muse
     :label "Muse Code"
     :require ai-code-muse-cli
     :start   ai-code-muse-cli
     :switch  ai-code-muse-cli-switch-to-buffer
     :send    ai-code-muse-cli-send-command
     :resume  ai-code-muse-cli-resume
     :config  nil
     :agent-file nil
     :install "curl -fsSL https://dev.meta.ai/install.sh | bash"
     :upgrade nil
     :install-skills nil
     :cli     "muse")
    (github-copilot-cli
     :label "GitHub Copilot CLI"
     :require ai-code-github-copilot-cli
     :start   ai-code-github-copilot-cli
     :switch  ai-code-github-copilot-cli-switch-to-buffer
     :send    ai-code-github-copilot-cli-send-command
     :resume  ai-code-github-copilot-cli-resume
     :config  "~/.copilot/mcp-config.json"
     :agent-file nil
     :install "npm install -g @github/copilot"
     :upgrade nil
     :install-skills nil
     :cli     "copilot")
    (codex
     :label "OpenAI Codex CLI"
     :require ai-code-codex-cli
     :start   ai-code-codex-cli
     :switch  ai-code-codex-cli-switch-to-buffer
     :send    ai-code-codex-cli-send-command
     :resume  ai-code-codex-cli-resume
     :config  "~/.codex/config.toml"
     :agent-file "AGENTS.md"
     :install "npm install -g @openai/codex@latest"
     :upgrade nil
     :install-skills nil
     :cli     "codex")
    (pi
     :label "Pi"
     :require ai-code-pi
     :start   ai-code-pi-start
     :switch  ai-code-pi-switch-to-buffer
     :send    ai-code-pi-send-command
     :resume  ai-code-pi-resume
     :config  "~/.pi/agent/settings.json"
     :agent-file "AGENTS.md"
     :install "npm install -g --ignore-scripts @earendil-works/pi-coding-agent"
     :upgrade "pi update --self"
     :install-skills nil
     :cli     "pi")
    (open-interpreter
     :label "Open Interpreter CLI"
     :require ai-code-open-interpreter-cli
     :start   ai-code-open-interpreter-cli
     :switch  ai-code-open-interpreter-cli-switch-to-buffer
     :send    ai-code-open-interpreter-cli-send-command
     :resume  ai-code-open-interpreter-cli-resume
     :config  "~/.openinterpreter/config.toml"
     :agent-file "AGENTS.md"
     :install nil
     :upgrade nil
     :install-skills nil
     :cli     "interpreter")
    (opencode
     :label "Opencode"
     :require ai-code-opencode
     :start   ai-code-opencode
     :switch  ai-code-opencode-switch-to-buffer
     :send    ai-code-opencode-send-command
     :resume  ai-code-opencode-resume
     :config  "~/.config/opencode/opencode.jsonc"
     :agent-file nil
     :install "npm i -g opencode-ai@latest"
     :upgrade nil
     :install-skills nil
     :cli     "opencode")
    (kilo
     :label "Kilo"
     :require ai-code-kilo
     :start   ai-code-kilo
     :switch  ai-code-kilo-switch-to-buffer
     :send    ai-code-kilo-send-command
     :resume  ai-code-kilo-resume
     :config  "~/.config/kilo/kilo.json"
     :agent-file nil
     :install "npm install -g kilo@latest"
     :upgrade nil
     :install-skills nil
     :cli     "kilo")
    (grok
     :label "Grok CLI"
     :require ai-code-grok-cli
     :start   ai-code-grok-cli
     :switch  ai-code-grok-cli-switch-to-buffer
     :send    ai-code-grok-cli-send-command
     :resume  ai-code-grok-cli-resume
     :config  "~/.config/grok/config.json"
     :agent-file nil
     :install "bun add -g @vibe-kit/grok-cli"
     :upgrade nil
     :install-skills nil
     :cli     "grok")
    (cursor
     :label "Cursor CLI"
     :require ai-code-cursor-cli
     :start   ai-code-cursor-cli
     :switch  ai-code-cursor-cli-switch-to-buffer
     :send    ai-code-cursor-cli-send-command
     :resume  ai-code-cursor-cli-resume
     :config  "~/.cursor"
     :agent-file nil
     :install "cursor-agent update"
     :upgrade nil
     :install-skills nil
     :cli     "cursor-agent")
    (kiro
     :label "Kiro CLI"
     :require ai-code-kiro-cli
     :start   ai-code-kiro-cli
     :switch  ai-code-kiro-cli-switch-to-buffer
     :send    ai-code-kiro-cli-send-command
     :resume  ai-code-kiro-cli-resume
     :config  "~/.kiro/settings/cli.json"
     :agent-file nil
     :install "kiro-cli update"
     :upgrade nil
     :install-skills nil
     :cli     "kiro-cli")
    (codebuddy
     :label "CodeBuddy Code"
     :require ai-code-codebuddy-cli
     :start   ai-code-codebuddy-cli
     :switch  ai-code-codebuddy-cli-switch-to-buffer
     :send    ai-code-codebuddy-cli-send-command
     :resume  ai-code-codebuddy-cli-resume
     :config  "~/.codebuddy"
     :agent-file nil
     :install "codebuddy update"
     :upgrade nil
     :install-skills nil
     :cli     "codebuddy")
    (aider
     :label "Aider CLI"
     :require ai-code-aider-cli
     :start   ai-code-aider-cli
     :switch  ai-code-aider-cli-switch-to-buffer
     :send    ai-code-aider-cli-send-command
     :resume  nil
     :config  "~/.aider.conf.yml"
     :agent-file nil
     :install nil
     :upgrade nil
     :install-skills nil
     :cli     "aider")
    (eca                      ; external backend, requires eca package
     :label "ECA (Editor Code Assistant)"
     :require ai-code-eca
     :start   ai-code-eca-start
     :switch  ai-code-eca-switch
     :send    ai-code-eca-send
     :resume  ai-code-eca-resume
     :config  "~/.config/eca/config.json"
     :agent-file "AGENTS.md"
     :install ai-code-eca-upgrade
     :upgrade nil
     :install-skills ai-code-eca-install-skills
     :cli     nil)
    (agent-shell      ; external backend, requires agent-shell package
     :label "agent-shell"
     :require ai-code-agent-shell
     :start   ai-code-agent-shell
     :switch  ai-code-agent-shell-switch-to-buffer
     :send    ai-code-agent-shell-send-command
     :resume  ai-code-agent-shell-resume
     :config  nil
     :agent-file nil
     :install nil
     :upgrade nil
     :install-skills nil
     :cli     "agent-shell")
    (gptel-agent      ; external backend, requires gptel-agent package
     :label "GPTel Agent"
     :require ai-code-gptel-agent
     :start   ai-code-gptel-agent
     :switch  ai-code-gptel-agent-switch-to-buffer
     :send    ai-code-gptel-agent-send-command
     :resume  nil
     :config  nil
     :agent-file nil
     :install nil
     :upgrade nil
     :install-skills nil
     :cli     nil)
    (claude-code-ide ; external backend, requires claude-code-ide.el package
     :label "claude-code-ide.el"
     :require claude-code-ide
     :start   claude-code-ide--start-if-no-session
     :switch  claude-code-ide-switch-to-buffer
     :send    claude-code-ide-send-prompt
     :resume  claude-code-ide-resume
     :config  "~/.claude.json"
     :agent-file "CLAUDE.md"
     :install "npm install -g @anthropic-ai/claude-code@latest"
     :upgrade nil
     :install-skills nil
     :cli     "claude")
    (claude-code-el ; external backend, requires claude-code.el package
     :label "claude-code.el"
     :require claude-code
     :start   claude-code
     :switch  claude-code-switch-to-buffer
     :send    ai-code-claude-code-el-send-command
     :resume  claude-code-resume
     :config  "~/.claude.json"
     :agent-file "CLAUDE.md"
     :install "npm install -g @anthropic-ai/claude-code@latest"
     :upgrade nil
     :install-skills nil
     :cli     "claude"))
  "Available AI backends and their integration metadata.
Each entry is a plist with backend labels, command functions,
configuration paths, install and upgrade commands, and skill-install commands."
  :type '(repeat (list (symbol :tag "Key")
                       (const :label) (string :tag "Label")
                       (const :require) (symbol :tag "Feature to require")
                       (const :start) (symbol :tag "Start function")
                       (const :switch) (symbol :tag "Switch function")
                       (const :send) (symbol :tag "Send function")
                       (const :resume) (choice (symbol :tag "Resume function")
                                               (const :tag "Not supported" nil))
                       (const :install) (choice (string :tag "Install command")
                                                (symbol :tag "Install function")
                                                (const :tag "Not supported" nil))
                       (const :upgrade) (choice (string :tag "Upgrade command")
                                                (symbol :tag "Upgrade function")
                                                (const :tag "Not supported" nil))
                       (const :cli) (string :tag "CLI name")
                       (const :agent-file) (choice (string :tag "Agent file name")
                                                   (const :tag "Not supported" nil))
                       (const :install-skills) (choice (string :tag "Install skills command")
                                                       (symbol :tag "Install skills function")
                                                       (const :tag "Not supported" nil))))
  :group 'ai-code)

(defcustom ai-code-backends-history-file
  (expand-file-name "ai-code-backends-history.el" user-emacs-directory)
  "File used to persist the MRU history of selected backends."
  :type 'file
  :group 'ai-code)

(defun ai-code--delete-backends-history-file ()
  "Delete `ai-code-backends-history-file' when it exists."
  (when (file-exists-p ai-code-backends-history-file)
    (ignore-errors (delete-file ai-code-backends-history-file))))

(defun ai-code--valid-backends-history-p (history)
  "Return non-nil when HISTORY is a list of backend symbols."
  (and (listp history)
       (seq-every-p (lambda (item)
                      (and item (symbolp item)))
                    history)))

(defun ai-code--load-backends-history ()
  "Load the MRU backend history from `ai-code-backends-history-file'."
  (if (file-exists-p ai-code-backends-history-file)
      (condition-case nil
          (with-temp-buffer
            (insert-file-contents ai-code-backends-history-file)
            (goto-char (point-min))
            (let ((history (read (current-buffer))))
              (skip-chars-forward " \t\n\r")
              (if (and (eobp)
                       (ai-code--valid-backends-history-p history))
                  (delete-dups history)
                (ai-code--delete-backends-history-file)
                nil)))
        (error
         (ai-code--delete-backends-history-file)
         nil))
    nil))

(defun ai-code--save-backend-history (backend)
  "Save BACKEND to the MRU history list file."
  (let* ((history (ai-code--load-backends-history))
         (updated-history
          (cons backend
                (seq-remove (lambda (history-backend)
                              (eq history-backend backend))
                            history))))
    (condition-case nil
        (with-temp-file ai-code-backends-history-file
          (insert (let ((print-circle nil))
                    (prin1-to-string updated-history))))
      (error nil))))

(defun ai-code-set-backend (new-backend)
  "Set the AI backend to NEW-BACKEND."
  (unless (ai-code--backend-spec new-backend)
    (user-error "Unknown backend: %s" new-backend))
  (setq ai-code-selected-backend new-backend)
  (ai-code--apply-backend new-backend)
  (ai-code--remember-current-backend-for-repo)
  (ai-code--save-backend-history new-backend))

(defun ai-code--backend-spec (key)
  "Return backend plist for KEY from `ai-code-backends'."
  (seq-find (lambda (it) (eq (car it) key)) ai-code-backends))

(defun ai-code--ordered-backend-choices ()
  "Return backend choices with the effective backend first, then MRU entries."
  (let* ((choices (mapcar (lambda (backend-spec)
                            (let* ((key (car backend-spec))
                                   (label (plist-get (cdr backend-spec) :label)))
                              (cons (format "%s" label) key)))
                          ai-code-backends))
         (effective-backend (ai-code--effective-backend))
         (history (ai-code--load-backends-history))
         (history-choices (seq-filter #'identity
                                      (mapcar (lambda (key)
                                                (seq-find (lambda (candidate)
                                                            (eq (cdr candidate) key))
                                                          choices))
                                              history)))
         (other-choices (seq-remove (lambda (candidate)
                                      (member candidate history-choices))
                                    choices))
         (sorted-choices (append history-choices other-choices))
         (current-choice (seq-find (lambda (candidate)
                                     (eq (cdr candidate) effective-backend))
                                   sorted-choices)))
    (if current-choice
        (cons current-choice
              (seq-remove (lambda (candidate)
                            (eq (cdr candidate) effective-backend))
                          sorted-choices))
      sorted-choices)))

(defun ai-code--backend-completion-table (candidates)
  "Return a completion table that preserves the order of CANDIDATES."
  (lambda (string predicate action)
    (if (eq action 'metadata)
        '(metadata
          (display-sort-function . identity)
          (cycle-sort-function . identity))
      (complete-with-action action candidates string predicate))))

(defun ai-code-current-backend-label ()
  "Return label string of the currently selected backend.
Falls back to symbol name when label is unavailable."
  (let* ((effective-backend (ai-code--effective-backend))
         (spec (ai-code--backend-spec effective-backend))
         (label (when spec (plist-get (cdr spec) :label))))
    (or label (and effective-backend
                   (symbol-name effective-backend)) "<none>")))

(defun ai-code--ensure-backend-loaded (spec)
  "Ensure FEATURE for backend SPEC is loaded, if any."
  (let* ((plist (cdr spec))
         (feature (plist-get plist :require)))
    (when feature (require feature nil t))))

(defun ai-code--apply-backend (key)
  "Apply backend identified by KEY.
Sets backend dispatch functions and updates `ai-code-cli'."
  (let* ((spec (ai-code--backend-spec key)))
    (unless spec
      (user-error "Unknown backend: %s" key))
    (ai-code--ensure-backend-loaded spec)
    (let* ((plist (cdr spec))
           (label  (plist-get plist :label))
           (feature (plist-get plist :require))
           (start  (plist-get plist :start))
           (switch (plist-get plist :switch))
           (send   (plist-get plist :send))
           (resume (plist-get plist :resume))
           (cli    (plist-get plist :cli)))
      ;; If the declared feature is not available after require,
      ;; inform user to install it.
      (when (and feature (not (featurep feature)))
        (user-error
         "Backend '%s' is not available.  Please install the package providing '%s' and try again"
         label (symbol-name feature)))
      (let ((missing-fns (seq-filter (lambda (fn) (not (fboundp fn)))
                                     (list start switch send))))
        (when missing-fns
          (user-error
           "Backend '%s' is not available (missing functions: %s).  Please install the package providing '%s'"
           label
           (mapconcat #'symbol-name missing-fns ", ")
           (symbol-name feature))))
      (when (and resume (not (fboundp resume)))
        (user-error
         "Backend '%s' declares resume function '%s' but it is not callable"
         label (symbol-name resume)))
      (setq ai-code--cli-start-fn start
            ai-code--cli-switch-fn switch
            ai-code--cli-send-fn send
            ai-code--cli-resume-fn (if resume
                                       (lambda (&optional arg)
                                         (interactive "P")
                                         (let ((current-prefix-arg (or arg current-prefix-arg)))
                                           (call-interactively resume)))
                                     #'ai-code--unsupported-resume))
      (setq ai-code-cli cli
            ai-code-selected-backend key)
      (message "AI Code backend switched to: %s" (plist-get plist :label)))))

;;;###autoload
(defun ai-code-cli-start-or-resume (&optional arg)
  "Start or resume the CLI depending on prefix argument.
If called with \\[universal-argument] (raw prefix ARG \\='(4)),
invoke `ai-code-cli-resume'; otherwise call `ai-code-cli-start'."
  (interactive "P")
  (if arg
      (call-interactively #'ai-code-cli-resume)
    (call-interactively #'ai-code-cli-start)))

;;;###autoload
(defun ai-code-select-backend ()
  "Interactively select and apply an AI backend from `ai-code-backends'."
  (interactive)
  (let* ((ordered-choices (ai-code--ordered-backend-choices))
         (current-choice (car ordered-choices))
         (completion-table
          (ai-code--backend-completion-table (mapcar #'car ordered-choices)))
         (completion-extra-properties
          '(:display-sort-function identity
            :cycle-sort-function identity))
         (helm-completion-styles-alist
          (cons '(ai-code-select-backend . emacs)
                (and (boundp 'helm-completion-styles-alist)
                     helm-completion-styles-alist)))
         (ivy-sort-functions-alist
          (cons '(ai-code-select-backend . nil)
                (and (boundp 'ivy-sort-functions-alist)
                     ivy-sort-functions-alist)))
         (vertico-sort-override-function nil)
         (choice (completing-read "Select backend: "
                                  completion-table
                                  nil t nil nil (car current-choice)))
         (key (cdr (assoc choice ordered-choices))))
    (ai-code-set-backend key)
    (when (fboundp 'ai-code-onboarding-show-backend-switch-hint)
      (ai-code-onboarding-show-backend-switch-hint))))

;;;###autoload
(defun ai-code-open-backend-config ()
  "Open the current backend's configuration file in another window."
  (interactive)
  (let* ((spec (ai-code--backend-spec ai-code-selected-backend)))
    (if (not spec)
        (user-error "No backend is currently selected")
      (let* ((plist  (cdr spec))
             (label  (or (plist-get plist :label)
                         (symbol-name ai-code-selected-backend)))
             (config (plist-get plist :config)))
        (if (not config)
            (user-error "Backend '%s' does not declare a config file" label)
          (let ((file (expand-file-name config)))
            (find-file-other-window file)
            (message "Opened %s config: %s" label file)))))))

;;;###autoload
(defun ai-code-open-backend-agent-file ()
  "Open the current backend's agent file from the git repository root."
  (interactive)
  (let* ((spec (ai-code--backend-spec ai-code-selected-backend)))
    (if (not spec)
        (user-error "No backend is currently selected")
      (let* ((plist (cdr spec))
             (label (or (plist-get plist :label)
                        (symbol-name ai-code-selected-backend)))
             (agent-file (plist-get plist :agent-file)))
        (if (not agent-file)
            (user-error "Backend '%s' does not declare an agent file" label)
          (let* ((git-root (ai-code--validate-git-repository))
                 (file (expand-file-name agent-file git-root)))
            (find-file-other-window file)
            (message "Opened %s agent file: %s" label file)))))))

;;;###autoload
(defun ai-code-upgrade-backend (&optional arg)
  "Install or upgrade the currently selected backend's CLI.
When the declared CLI is unavailable, use :install.  Otherwise, use
:upgrade when defined and fall back to :install.  String commands run
via `compile'; function symbols receive prefix ARG interactively."
  (interactive "P")
  (let* ((spec (ai-code--backend-spec ai-code-selected-backend)))
    (if (not spec)
        (user-error "No backend is currently selected")
      (let* ((plist (cdr spec))
             (install (plist-get plist :install))
             (upgrade (plist-get plist :upgrade))
             (cli (plist-get plist :cli))
             (label (ai-code-current-backend-label))
             (cli-available (and (stringp cli) (executable-find cli)))
             (command (if cli-available (or upgrade install) install))
             (property (if (and cli-available upgrade) :upgrade :install)))
        (when (and command (symbolp command))
          (ai-code--ensure-backend-loaded spec))
        (cond
         ((stringp command)
          (compile command)
          (message "Running %s command for %s" property label))
         ((and command (symbolp command) (fboundp command))
          (let ((current-prefix-arg arg))
            (call-interactively command))
          (message "Running %s for %s" property label))
         ((and command (symbolp command) (not (fboundp command)))
          (user-error "Backend '%s' declares %s function '%s' but it is not callable"
                      label property command))
         ((and (null install) (null upgrade))
          (user-error "Backend '%s' defines neither :install nor :upgrade command"
                      label))
         ((not install)
          (user-error "Backend '%s' CLI '%s' is unavailable and :install is not defined"
                      label (or cli "<none>")))
         (t
          (user-error "Backend '%s' declares invalid %s command: %S"
                      label property command)))))))

(defun ai-code--install-backend-skills-fallback (label)
  "Fallback skills installation for backend LABEL.
Prompt user for a skills repository URL and ask the AI CLI session
to read the repo README and install the skills."
  (ai-code--manage-backend-skills-fallback label 'install))

(defun ai-code--manage-backend-skills-fallback (label action)
  "Fallback backend skills management for LABEL and ACTION.
ACTION should be the symbol `install' or `uninstall'."
  (let* ((action-name
          (pcase action
            ('install "install")
            ('uninstall "uninstall")
            (_ (user-error
                "Invalid backend skills action: %S; expected `install' or `uninstall'"
                action))))
         (url (read-string
               (format "Skills repo URL for %s %s: " label action-name)
               nil nil "https://github.com/obra/superpowers"))
         (default-prompt
          (if (eq action 'uninstall)
              (format
               "Please read the README of %s and uninstall/remove the skills described there for %s globally for the current user, not just for the current project. Follow the repository instructions to remove the skill files from the backend's user-level/global skills directory and clean up related global configuration. Do not remove or modify project-specific skill files or configuration."
               url label)
            (format
             "Please read the README of %s and install/setup the skills described there for %s globally for the current user so they are available to all projects. Follow the installation instructions in the README, using the backend's user-level/global skills directory. Do not install or modify skill files or configuration in the current project."
             url label)))
         (prompt (if (called-interactively-p 'interactive)
                     (ai-code-read-string
                      (format "Edit %s-skills prompt for %s: " action-name label)
                      default-prompt)
                   default-prompt)))
    (ai-code-cli-send-command prompt)))

;;;###autoload
(defun ai-code-install-backend-skills ()
  "Install or uninstall skills for the currently selected backend.
Prompt for whether to install or uninstall first.
When installing, if the backend defines an :install-skills property, use it:
  - string: run as a shell command via `compile'.
  - symbol: call the function.
Otherwise, or when uninstalling, fall back to prompting the AI session
to manage skills from a skills repository URL."
  (interactive)
  (let* ((spec (ai-code--backend-spec ai-code-selected-backend)))
    (if (not spec)
        (user-error "No backend is currently selected")
      (ai-code--ensure-backend-loaded spec)
      (let* ((plist (cdr spec))
             (install-skills (plist-get plist :install-skills))
             (label (ai-code-current-backend-label))
             (action-choice (completing-read
                             (format "Manage skills for %s: " label)
                             '("install" "uninstall")
                             nil t nil nil "install"))
             (action (if (string= action-choice "uninstall")
                         'uninstall
                       'install)))
        (cond
         ((eq action 'uninstall)
          (ai-code--manage-backend-skills-fallback label 'uninstall))
         ((stringp install-skills)
          (compile install-skills)
          (message "Running skills installation for %s" label))
         ((and install-skills (symbolp install-skills) (fboundp install-skills))
          (funcall install-skills)
          (message "Running skills installation for %s" label))
         ((and install-skills (symbolp install-skills) (not (fboundp install-skills)))
          (user-error "Backend '%s' declares :install-skills function '%s' but it is not callable"
                      label install-skills))
         (t
          (ai-code--install-backend-skills-fallback label)))))))

(provide 'ai-code-backends)

;;; ai-code-backends.el ends here
