package cli

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/spf13/cobra"

	"github.com/aoagents/agent-orchestrator/backend/internal/config"
	"github.com/aoagents/agent-orchestrator/backend/internal/legacyimport"
	"github.com/aoagents/agent-orchestrator/backend/internal/runfile"
	"github.com/aoagents/agent-orchestrator/backend/internal/storage/sqlite"
)

type importOptions struct {
	from   string
	dryRun bool
	yes    bool
	json   bool
}

func newImportCommand(ctx *commandContext) *cobra.Command {
	var opts importOptions
	cmd := &cobra.Command{
		Use:   "import",
		Short: "Import projects and orchestrator sessions from a legacy AO install",
		Long: "Import reads the legacy Agent Orchestrator flat-file store " +
			"(~/.agent-orchestrator) read-only and ports its projects, per-project " +
			"settings, and each project's live orchestrator session into the rewrite " +
			"database. Legacy files are never modified, and a re-run skips rows that " +
			"already exist, so it is safe to run more than once.\n\n" +
			"The daemon must be stopped: it is the sole writer of the database.",
		Args: noArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			return ctx.runImport(cmd, opts)
		},
	}
	cmd.Flags().StringVar(&opts.from, "from", "", "Legacy AO root to read (default ~/.agent-orchestrator)")
	cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "Parse and report the planned import without writing")
	cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, "Skip the confirmation prompt (for non-interactive use)")
	cmd.Flags().BoolVar(&opts.json, "json", false, "Output the import report as JSON")
	return cmd
}

func (c *commandContext) runImport(cmd *cobra.Command, opts importOptions) error {
	cfg, err := config.Load()
	if err != nil {
		return err
	}

	// The daemon is the sole writer; refuse to open the store underneath a live
	// one. A stale run-file (dead PID) is treated as safe.
	if live, err := runfile.CheckStale(cfg.RunFilePath); err != nil {
		return fmt.Errorf("inspect run-file: %w", err)
	} else if live != nil {
		return usageError{fmt.Errorf("the AO daemon is running (pid %d); stop it first with `ao stop` before importing", live.PID)}
	}

	root := opts.from
	if root == "" {
		root = legacyimport.DefaultLegacyRootDir()
	}
	if !legacyimport.HasLegacyData(root) {
		_, err := fmt.Fprintf(cmd.OutOrStdout(), "No legacy AO projects found at %s. Nothing to import.\n", root)
		return err
	}

	if !opts.dryRun && !opts.yes {
		ok, err := confirm(c.deps.In, cmd.OutOrStdout(),
			fmt.Sprintf("Import projects and orchestrator sessions from %s?", root), true)
		if err != nil {
			return err
		}
		if !ok {
			_, err := fmt.Fprintln(cmd.OutOrStdout(), "Import cancelled.")
			return err
		}
	}

	rep, err := c.executeImport(cmd.Context(), cfg, legacyimport.Options{
		Root:    root,
		DataDir: cfg.DataDir,
		DryRun:  opts.dryRun,
	})
	if err != nil {
		return err
	}

	if opts.json {
		return writeJSON(cmd.OutOrStdout(), rep)
	}
	return writeImportSummary(cmd.OutOrStdout(), rep)
}

// executeImport opens the rewrite store, runs the import, and closes the store.
// It is the one CLI path that opens the database directly: the import is a
// one-time bootstrap that must run with the daemon stopped (guarded by the
// caller), so it cannot go through the daemon's loopback API.
func (c *commandContext) executeImport(ctx context.Context, cfg config.Config, opts legacyimport.Options) (legacyimport.Report, error) {
	store, err := sqlite.Open(cfg.DataDir)
	if err != nil {
		return legacyimport.Report{}, fmt.Errorf("open store: %w", err)
	}
	defer func() { _ = store.Close() }()
	return legacyimport.Run(ctx, store, opts)
}

func writeImportSummary(w io.Writer, rep legacyimport.Report) error {
	var b strings.Builder
	if rep.DryRun {
		b.WriteString("Dry run — no changes written.\n")
	}
	fmt.Fprintf(&b, "Projects:       %d imported, %d already present\n", rep.ProjectsImported, rep.ProjectsSkipped)
	fmt.Fprintf(&b, "Orchestrators:  %d imported, %d skipped, %d absent\n", rep.OrchestratorsImported, rep.OrchestratorsSkipped, rep.OrchestratorsAbsent)
	fmt.Fprintf(&b, "Transcripts:    %d relocated\n", rep.TranscriptsRelocated)
	if len(rep.Notes) > 0 {
		b.WriteString("\nNotes:\n")
		for _, n := range rep.Notes {
			fmt.Fprintf(&b, "  - %s\n", n)
		}
	}
	_, err := io.WriteString(w, b.String())
	return err
}

// confirm prompts for a yes/no answer. When stdin is not an interactive
// terminal it returns the default without prompting, so headless invocations
// behave deterministically.
func confirm(in io.Reader, out io.Writer, prompt string, defaultYes bool) (bool, error) {
	suffix := " [Y/n] "
	if !defaultYes {
		suffix = " [y/N] "
	}
	if !stdinIsInteractive(in) {
		return defaultYes, nil
	}
	if _, err := io.WriteString(out, prompt+suffix); err != nil {
		return false, err
	}
	line, err := bufio.NewReader(in).ReadString('\n')
	if err != nil && line == "" {
		// EOF with no input: fall back to the default.
		return defaultYes, nil
	}
	switch strings.ToLower(strings.TrimSpace(line)) {
	case "":
		return defaultYes, nil
	case "y", "yes":
		return true, nil
	case "n", "no":
		return false, nil
	default:
		return false, nil
	}
}

// stdinIsInteractive reports whether in is an interactive terminal. It only
// treats the real os.Stdin as potentially interactive; a piped reader or test
// buffer is non-interactive.
func stdinIsInteractive(in io.Reader) bool {
	f, ok := in.(*os.File)
	if !ok {
		return false
	}
	info, err := f.Stat()
	if err != nil {
		return false
	}
	return info.Mode()&os.ModeCharDevice != 0
}
