package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"reasonix/internal/command"
	"reasonix/internal/config"
	"reasonix/internal/hook"
	"reasonix/internal/installsource"
	"reasonix/internal/pluginpkg"
)

type PluginView struct {
	Name                string                         `json:"name"`
	Version             string                         `json:"version,omitempty"`
	Description         string                         `json:"description,omitempty"`
	Source              string                         `json:"source,omitempty"`
	Root                string                         `json:"root"`
	ManifestKind        string                         `json:"manifestKind,omitempty"`
	Enabled             bool                           `json:"enabled"`
	Skills              int                            `json:"skills"`
	Commands            int                            `json:"commands"`
	Hooks               int                            `json:"hooks"`
	MCPServers          int                            `json:"mcpServers"`
	Agents              int                            `json:"agents,omitempty"`
	Compatibility       string                         `json:"compatibility,omitempty"`
	MappedCapabilities  []string                       `json:"mappedCapabilities,omitempty"`
	SkippedCapabilities []pluginpkg.CompatibilityIssue `json:"skippedCapabilities,omitempty"`
	SkillDetails        []PluginSkillView              `json:"skillDetails,omitempty"`
	AgentDetails        []PluginAgentView              `json:"agentDetails,omitempty"`
	CommandDetails      []PluginCommandView            `json:"commandDetails,omitempty"`
	HookDetails         []PluginHookView               `json:"hookDetails,omitempty"`
	MCPServerDetails    []PluginMCPServerView          `json:"mcpServerDetails,omitempty"`
	Warnings            []string                       `json:"warnings,omitempty"`
	Error               string                         `json:"error,omitempty"`
}

type PluginInstallOptions struct {
	DryRun  bool   `json:"dryRun,omitempty"`
	Link    bool   `json:"link,omitempty"`
	Replace bool   `json:"replace,omitempty"`
	Name    string `json:"name,omitempty"`
}

type PluginSkillView struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Path        string `json:"path,omitempty"`
	Invocation  string `json:"invocation,omitempty"`
	RunAs       string `json:"runAs,omitempty"`
}

type PluginAgentView struct {
	Name         string   `json:"name"`
	Description  string   `json:"description,omitempty"`
	Path         string   `json:"path,omitempty"`
	Invocation   string   `json:"invocation,omitempty"`
	Model        string   `json:"model,omitempty"`
	AllowedTools []string `json:"allowedTools,omitempty"`
}

type PluginCommandView struct {
	Name             string `json:"name"`
	Description      string `json:"description,omitempty"`
	ArgHint          string `json:"argHint,omitempty"`
	Path             string `json:"path,omitempty"`
	Invocation       string `json:"invocation,omitempty"`
	Shadowed         bool   `json:"shadowed,omitempty"`
	ShadowedByPlugin string `json:"shadowedByPlugin,omitempty"`
}

type PluginHookView struct {
	Event       string `json:"event"`
	Match       string `json:"match,omitempty"`
	Command     string `json:"command,omitempty"`
	ContextFile string `json:"contextFile,omitempty"`
	Description string `json:"description,omitempty"`
}

type PluginMCPServerView struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName,omitempty"`
	Description string `json:"description,omitempty"`
	Transport   string `json:"transport,omitempty"`
	Command     string `json:"command,omitempty"`
	URL         string `json:"url,omitempty"`
	AutoStart   bool   `json:"autoStart,omitempty"`
}

func (a *App) Plugins() []PluginView {
	st, err := pluginpkg.LoadState(config.ReasonixHomeDir())
	if err != nil {
		return []PluginView{{Error: err.Error()}}
	}
	a.mu.RLock()
	ctrl := a.activeCtrlLocked()
	a.mu.RUnlock()
	var activeCommands []command.Command
	if ctrl != nil {
		activeCommands = ctrl.Commands()
	}
	out := make([]PluginView, 0, len(st.Plugins))
	for _, p := range st.Plugins {
		view := PluginView{
			Name:         p.Name,
			Version:      p.Version,
			Description:  p.Description,
			Source:       p.Source,
			Root:         pluginpkg.ResolveRoot(config.ReasonixHomeDir(), p.Root),
			ManifestKind: p.ManifestKind,
			Enabled:      p.Enabled,
		}
		if pkg, warnings, err := pluginpkg.ParseDir(view.Root); err == nil {
			applyPluginPackageDetails(&view, pkg, warnings)
			decoratePluginCommandConflicts(&view, activeCommands)
		} else {
			view.Error = err.Error()
		}
		out = append(out, view)
	}
	return out
}

func decoratePluginCommandConflicts(view *PluginView, commands []command.Command) {
	if view == nil || !view.Enabled || len(view.CommandDetails) == 0 || len(commands) == 0 {
		return
	}
	byName := make(map[string]command.Command, len(commands))
	for _, cmd := range commands {
		byName[cmd.Name] = cmd
	}
	for i := range view.CommandDetails {
		detail := &view.CommandDetails[i]
		qualified := view.Name + ":" + detail.Name
		winner, ok := byName[qualified]
		if !ok || winner.Plugin == view.Name && winner.ShortName == detail.Name && !winner.Hidden {
			continue
		}
		detail.Shadowed = true
		detail.ShadowedByPlugin = winner.Plugin
	}
}

func applyPluginPackageDetails(view *PluginView, pkg pluginpkg.Package, warnings []string) {
	view.Skills, view.Commands, view.Hooks, view.MCPServers = pkg.CapabilityCounts()
	view.Agents = pkg.AgentCount()
	view.Compatibility = pkg.Compatibility.Status
	view.MappedCapabilities = append([]string(nil), pkg.Compatibility.Mapped...)
	view.SkippedCapabilities = append([]pluginpkg.CompatibilityIssue(nil), pkg.Compatibility.Skipped...)
	view.Warnings = warnings
	inv := pkg.Inventory()
	view.CommandDetails = make([]PluginCommandView, 0, len(inv.Commands))
	for _, cmd := range inv.Commands {
		view.CommandDetails = append(view.CommandDetails, PluginCommandView{
			Name:        cmd.Name,
			Description: cmd.Description,
			ArgHint:     cmd.ArgHint,
			Path:        cmd.Path,
			Invocation:  "/" + view.Name + ":" + cmd.Name,
		})
	}
	view.SkillDetails = make([]PluginSkillView, 0, len(inv.Skills))
	for _, sk := range inv.Skills {
		view.SkillDetails = append(view.SkillDetails, PluginSkillView{
			Name:        sk.Name,
			Description: sk.Description,
			Path:        sk.Path,
			Invocation:  "/" + view.Name + ":" + sk.Name,
			RunAs:       sk.RunAs,
		})
	}
	view.AgentDetails = make([]PluginAgentView, 0, len(inv.Agents))
	for _, agent := range inv.Agents {
		view.AgentDetails = append(view.AgentDetails, PluginAgentView{
			Name: agent.Name, Description: agent.Description, Path: agent.Path,
			Invocation: "/" + view.Name + ":agent:" + agent.Name, Model: agent.Model,
			AllowedTools: append([]string(nil), agent.AllowedTools...),
		})
	}
	view.HookDetails = make([]PluginHookView, 0, len(inv.Hooks))
	for _, hook := range inv.Hooks {
		view.HookDetails = append(view.HookDetails, PluginHookView{
			Event:       hook.Event,
			Match:       hook.Match,
			Command:     hook.Command,
			ContextFile: hook.ContextFile,
			Description: hook.Description,
		})
	}
	view.MCPServerDetails = make([]PluginMCPServerView, 0, len(inv.MCPServers))
	for _, server := range inv.MCPServers {
		view.MCPServerDetails = append(view.MCPServerDetails, PluginMCPServerView{
			Name: server.Name, DisplayName: server.DisplayName, Description: server.Description,
			Transport: server.Transport, Command: server.Command, URL: server.URL, AutoStart: server.AutoStart,
		})
	}
}

func (a *App) PlanPluginInstall(source string, opts PluginInstallOptions) (string, error) {
	opts.DryRun = true
	return a.runPluginInstallSource(source, opts, false)
}

func (a *App) InstallPlugin(source string, opts PluginInstallOptions) (string, error) {
	if err := a.ensureActiveTabRebuildAllowed("plugins"); err != nil {
		return "", err
	}
	out, err := a.runPluginInstallSource(source, opts, true)
	if err != nil {
		return "", err
	}
	a.bumpExtensionGeneration()
	a.invalidateSkillRootsCache()
	if rebuildErr := a.rebuild(); rebuildErr != nil {
		if _, ok := a.deferredRebuildWarning("plugins", rebuildErr); ok {
			return out, nil
		}
		return out, rebuildErr
	}
	return out, nil
}

func (a *App) RemovePlugin(name string) error {
	if err := a.ensureActiveTabRebuildAllowed("plugins"); err != nil {
		return err
	}
	// Uninstall disconnects the plugin's MCP servers, so the whole flow holds
	// the MCP lifecycle lock: an unlocked disconnect can interleave with a
	// launch-authorization preflight, which would then relaunch the just-removed server from
	// its stale snapshot.
	defer a.lockMCPMutation("remove-plugin")()
	// A global uninstall touches every runtime, so gate every visible and
	// detached tab — not only the active one — and hold the gates through the
	// uninstall and rebuild. The re-check runs under the gates because the
	// lifecycle-lock wait can outlast the pre-lock check: work that started
	// mid-wait must fail the removal before anything is deleted, and no tab
	// may start a turn against a half-removed plugin.
	releaseGates, err := a.lockRuntimeTurnGates("plugins", nil)
	if err != nil {
		return err
	}
	defer releaseGates()
	tab := a.activeTab()
	if tab == nil && a.ctx != nil {
		return fmt.Errorf("no active tab")
	}
	raw, _ := json.Marshal(map[string]any{"op": "uninstall", "kind": "plugin", "name": strings.TrimSpace(name), "scope": "global"})
	tl := installsource.NewTool(installsource.Options{
		ProjectRoot:  a.activeWorkspaceRoot(),
		OnDisconnect: a.disconnectMCPServerAllRuntimes,
	})
	if _, err := tl.Execute(context.Background(), raw); err != nil {
		return err
	}
	a.bumpExtensionGeneration()
	a.invalidateSkillRootsCache()
	if tab == nil || a.ctx == nil {
		return nil
	}
	if err := a.rebuildSettingTurnLocked("plugins", tab, true, false); err != nil {
		if _, ok := a.deferredRebuildWarning("plugins", err); ok {
			return nil
		}
		return err
	}
	return nil
}

func (a *App) SetPluginEnabled(name string, enabled bool) error {
	if err := a.ensureActiveTabRebuildAllowed("plugins"); err != nil {
		return err
	}
	if err := pluginpkg.SetEnabled(config.ReasonixHomeDir(), strings.TrimSpace(name), enabled); err != nil {
		return err
	}
	a.bumpExtensionGeneration()
	a.invalidateSkillRootsCache()
	if err := a.rebuild(); err != nil {
		if _, ok := a.deferredRebuildWarning("plugins", err); ok {
			return nil
		}
		return err
	}
	return nil
}

func (a *App) UpdatePlugin(name string) (string, error) {
	name = strings.TrimSpace(name)
	for _, p := range a.Plugins() {
		if p.Name == name {
			if strings.TrimSpace(p.Source) == "" {
				return "", fmt.Errorf("plugin %q has no recorded source", name)
			}
			return a.InstallPlugin(p.Source, PluginInstallOptions{Name: name, Replace: true})
		}
	}
	return "", fmt.Errorf("plugin %q is not installed", name)
}

func (a *App) PluginDoctor(name string) PluginView {
	name = strings.TrimSpace(name)
	for _, p := range a.Plugins() {
		if p.Name != name {
			continue
		}
		if p.Error != "" {
			return p
		}
		if p.Root == "" {
			p.Error = "missing plugin root"
			return p
		}
		if _, err := os.Stat(p.Root); err != nil {
			p.Error = err.Error()
			return p
		}
		pkg, _, err := pluginpkg.ParseDir(p.Root)
		if err != nil {
			p.Error = err.Error()
			return p
		}
		cfg, _ := config.LoadForRootReadOnly(a.activeWorkspaceRoot())
		runtimeOptions := hook.RuntimeOptions{}
		if cfg != nil {
			runtimeOptions = hook.RuntimeOptionsForShell(cfg.Tools.Shell.Prefer, cfg.Tools.Shell.Path)
		}
		for _, issue := range hook.CheckPackageRuntime(pkg, runtimeOptions) {
			p.Warnings = append(p.Warnings, fmt.Sprintf(
				"%s hook is unavailable: %v; install Git for Windows or configure a usable Bash path",
				issue.Event, issue.Err,
			))
		}
		return p
	}
	return PluginView{Name: name, Error: "plugin is not installed"}
}

func (a *App) runPluginInstallSource(source string, opts PluginInstallOptions, apply bool) (string, error) {
	mode := "copy"
	if opts.Link {
		mode = "link"
	}
	body := map[string]any{
		"source":  strings.TrimSpace(source),
		"kind":    "plugin",
		"mode":    mode,
		"replace": opts.Replace,
		"apply":   apply && !opts.DryRun,
	}
	if strings.TrimSpace(opts.Name) != "" {
		body["name"] = strings.TrimSpace(opts.Name)
	}
	raw, _ := json.Marshal(body)
	tl := installsource.NewTool(installsource.Options{ProjectRoot: a.activeWorkspaceRoot()})
	return tl.Execute(context.Background(), raw)
}
