//! synops-agent — modell-agnostisk agent-runtime.
//!
//! Erstatter Claude Code. Støtter OpenRouter, Anthropic, Gemini,
//! xAI, OpenAI og Ollama. Tool-loop med fil-operasjoner og shell.
//!
//! Bruk:
//!   synops-agent --model openrouter/anthropic/claude-sonnet-4 --task "fiks buggen"
//!   synops-agent --interactive
//!   synops-agent -i --model gemini/gemini-2.5-flash

mod checkpoint;
mod context;
mod daemon;
mod git;
mod graph;
mod provider;
mod tools;

use clap::Parser;
use context::{CompactionConfig, CompactionLevel, check_compaction_level, compact_messages};
use provider::{
    ApiKeys, LlmProvider, Message, RetryConfig, TokenUsage,
    calculate_cost, complete_with_retry, create_provider,
};
use sqlx::PgPool;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

#[derive(Parser)]
#[command(name = "synops-agent", about = "Modell-agnostisk agent-runtime for Synops")]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,

    /// Modell: "provider/model" (f.eks. "openrouter/anthropic/claude-sonnet-4")
    #[arg(short, long, default_value = "openrouter/anthropic/claude-sonnet-4")]
    model: String,

    /// Oppgave å utføre (påkrevd i batch-modus)
    #[arg(short, long)]
    task: Option<String>,

    /// Interaktiv REPL-modus
    #[arg(short, long)]
    interactive: bool,

    /// System-prompt (valgfritt, legges til før oppgaven)
    #[arg(short, long)]
    system: Option<String>,

    /// Arbeidsmappe (default: gjeldende)
    #[arg(short = 'd', long, default_value = ".")]
    working_dir: PathBuf,

    /// Maks antall tool-loop iterasjoner per turn
    #[arg(long, default_value = "50")]
    max_iterations: usize,

    /// Vis token-forbruk underveis
    #[arg(long)]
    verbose: bool,

    /// Spawn Claude Code i stedet (bruker abonnement)
    #[arg(long)]
    claude: bool,

    /// Maks kostnad i USD (f.eks. 0.50). Stopper og rapporterer ved grense.
    #[arg(long)]
    max_cost: Option<f64>,

    /// Maks output-tokens per LLM-kall (overstyrer provider-default)
    #[arg(long)]
    max_tokens: Option<u32>,

    /// Maks antall retries ved API-feil (default: 3)
    #[arg(long, default_value = "3")]
    max_retries: u32,

    /// Tving planmodus: tenk → plan → selvkritikk → utfør
    #[arg(long)]
    plan: bool,

    /// Deaktiver automatisk planmodus (brukes normalt for komplekse oppgaver)
    #[arg(long)]
    no_plan: bool,

    /// Commit-melding etter fullført oppgave (auto-generert hvis utelatt)
    #[arg(long)]
    commit_msg: Option<String>,

    /// Ikke auto-commit etter fullført oppgave
    #[arg(long)]
    no_commit: bool,

    /// Push etter commit (default: true, bruk --no-push for å deaktivere)
    #[arg(long, default_value = "true", action = clap::ArgAction::Set)]
    push: bool,

    /// Deaktiver push etter commit
    #[arg(long)]
    no_push: bool,

    /// Opprett og bytt til en egen branch for oppgaven
    #[arg(long)]
    branch: Option<String>,

    /// Gjenoppta en krasjet sesjon (sesjons-ID eller "latest")
    #[arg(long)]
    resume: Option<String>,

    /// Checkpoint-intervall: lagre mellomtilstand etter N tool-kall (default: 10)
    #[arg(long, default_value = "10")]
    checkpoint_interval: usize,
}

#[derive(clap::Subcommand)]
enum Command {
    /// Kjør som daemon/vaktmester (bakgrunnsprosess)
    Daemon {
        /// Poll-intervall i sekunder (default: 30)
        #[arg(long, default_value = "30")]
        interval: u64,

        /// Modell for oppgaver (default: openrouter/anthropic/claude-sonnet-4)
        #[arg(long, default_value = "openrouter/anthropic/claude-sonnet-4")]
        model: String,

        /// Arbeidsmappe (default: /home/vegard/synops)
        #[arg(short = 'd', long, default_value = "/home/vegard/synops")]
        working_dir: PathBuf,

        /// Sti til heartbeat-fil
        #[arg(long, default_value = "/tmp/synops-agent-daemon.heartbeat")]
        heartbeat: PathBuf,

        /// Sti til PID-fil
        #[arg(long, default_value = "/tmp/synops-agent-daemon.pid")]
        pid_file: PathBuf,

        /// Maks kostnad per oppgave i USD
        #[arg(long, default_value = "0.50")]
        max_cost_per_task: f64,

        /// Maks iterasjoner per oppgave
        #[arg(long, default_value = "50")]
        max_iterations: usize,

        /// Vis detaljert logging
        #[arg(long)]
        verbose: bool,
    },
}

/// Shared state for the agent session.
pub(crate) struct AgentSession {
    pub(crate) provider: Box<dyn LlmProvider>,
    pub(crate) messages: Vec<Message>,
    pub(crate) tool_defs: Vec<provider::ToolDef>,
    pub(crate) total_usage: HashMap<String, TokenUsage>,
    pub(crate) total_cost: f64,
    pub(crate) total_iterations: usize,
    pub(crate) retry_config: RetryConfig,
    pub(crate) compaction_config: CompactionConfig,
    pub(crate) working_dir: PathBuf,
    pub(crate) max_iterations: usize,
    pub(crate) max_cost: Option<f64>,
    pub(crate) verbose: bool,
    /// Set to true by Ctrl+C handler during a turn; checked by tool execution.
    pub(crate) interrupted: Arc<AtomicBool>,
    /// Optional PG pool for graph integration (synops_query tool, task picking).
    pub(crate) pg_pool: Option<PgPool>,
    /// Session ID for checkpoint/recovery.
    pub(crate) session_id: String,
    /// Model spec string (for checkpoint metadata).
    pub(crate) model_spec: String,
    /// Current task text (for checkpoint/report).
    pub(crate) task_text: Option<String>,
    /// Task node ID if from graph.
    pub(crate) task_node_id: Option<String>,
    /// How often to save checkpoint (every N tool calls).
    pub(crate) checkpoint_interval: usize,
    /// Counter for tool calls since last checkpoint.
    pub(crate) tool_calls_since_checkpoint: usize,
    /// When the session started.
    pub(crate) started_at: chrono::DateTime<chrono::Utc>,
}

/// Whether to use plan mode for a task.
#[derive(Debug, PartialEq)]
enum PlanDecision {
    /// No planning needed — execute directly.
    Direct,
    /// Use plan mode (Extended ReAct Loop).
    Plan,
}

/// Result of running one turn of the agent loop.
pub(crate) enum TurnResult {
    /// Agent finished (no more tool calls).
    Done,
    /// Budget exhausted.
    BudgetExhausted,
    /// Max iterations hit.
    MaxIterations,
    /// Interrupted by Ctrl+C.
    Interrupted,
}

impl AgentSession {
    /// Run the agent loop for the current messages until the model stops calling tools.
    pub(crate) async fn run_turn(&mut self) -> Result<TurnResult, Box<dyn std::error::Error>> {
        let mut iteration = 0;
        self.interrupted.store(false, Ordering::SeqCst);

        loop {
            iteration += 1;
            self.total_iterations += 1;

            if iteration > self.max_iterations {
                tracing::warn!("Nådde maks iterasjoner ({})", self.max_iterations);
                return Ok(TurnResult::MaxIterations);
            }

            // Budget check
            if let Some(max_cost) = self.max_cost {
                if self.total_cost >= max_cost {
                    tracing::warn!(
                        cost = format!("${:.4}", self.total_cost),
                        budget = format!("${:.2}", max_cost),
                        "Budsjettgrense nådd — stopper"
                    );
                    return Ok(TurnResult::BudgetExhausted);
                }
            }

            // Check if interrupted before calling LLM
            if self.interrupted.load(Ordering::SeqCst) {
                return Ok(TurnResult::Interrupted);
            }

            // Call LLM with retry
            let response = complete_with_retry(
                self.provider.as_ref(),
                &self.messages,
                &self.tool_defs,
                &self.retry_config,
            )
            .await?;

            // Accumulate token usage and cost
            let entry = self
                .total_usage
                .entry(response.model.clone())
                .or_insert_with(TokenUsage::default);
            entry.input_tokens += response.usage.input_tokens;
            entry.output_tokens += response.usage.output_tokens;

            let call_cost = calculate_cost(&response.model, &response.usage);
            self.total_cost += call_cost;

            if self.verbose {
                tracing::info!(
                    iteration,
                    input = response.usage.input_tokens,
                    output = response.usage.output_tokens,
                    call_cost = format!("${:.4}", call_cost),
                    total_cost = format!("${:.4}", self.total_cost),
                    "LLM-kall"
                );
            }

            // Adaptive Context Compaction
            let level =
                check_compaction_level(response.usage.input_tokens, &self.compaction_config);
            if level != CompactionLevel::None {
                let ratio = response.usage.input_tokens as f64
                    / self.compaction_config.context_window as f64;
                tracing::warn!(
                    prompt_tokens = response.usage.input_tokens,
                    context_window = self.compaction_config.context_window,
                    ratio = format!("{:.1}%", ratio * 100.0),
                    level = ?level,
                    "ACC: kontekstkomprimering trigget"
                );
                compact_messages(&mut self.messages, level, &self.compaction_config);
            }

            // Check for tool calls
            let has_tool_calls = response
                .message
                .tool_calls
                .as_ref()
                .is_some_and(|tc| !tc.is_empty());

            // Print any text response
            if let Some(ref text) = response.message.content {
                if !text.is_empty() {
                    println!("{}", text);
                }
            }

            // Add assistant message to history
            self.messages.push(response.message.clone());

            if !has_tool_calls {
                return Ok(TurnResult::Done);
            }

            // Execute tool calls
            let tool_calls = response.message.tool_calls.unwrap();
            for tc in &tool_calls {
                // Check interrupt before each tool call
                if self.interrupted.load(Ordering::SeqCst) {
                    // Add a synthetic tool result so the conversation stays valid
                    self.messages.push(Message {
                        role: "tool".into(),
                        content: Some("[interrupted by user]".into()),
                        tool_calls: None,
                        tool_call_id: Some(tc.id.clone()),
                    });
                    return Ok(TurnResult::Interrupted);
                }

                let args: serde_json::Value =
                    serde_json::from_str(&tc.function.arguments).unwrap_or_default();

                tracing::info!(tool = tc.function.name, "Kjører verktøy");

                let result =
                    tools::execute_tool(&tc.function.name, &args, &self.working_dir, self.pg_pool.as_ref()).await;

                if self.verbose {
                    let preview = if result.len() > 200 {
                        format!("{}...", &result[..200])
                    } else {
                        result.clone()
                    };
                    tracing::info!(tool = tc.function.name, result = preview, "Resultat");
                }

                // Add tool result to messages
                self.messages.push(Message {
                    role: "tool".into(),
                    content: Some(result),
                    tool_calls: None,
                    tool_call_id: Some(tc.id.clone()),
                });

                // Periodic checkpoint
                self.maybe_checkpoint();
            }
        }
    }

    /// Run the Extended ReAct Loop: think → plan → self-critique → execute.
    ///
    /// 1. Ask the LLM to analyze the task and create a step-by-step plan
    /// 2. Ask the LLM to critique its own plan
    /// 3. Execute with the refined plan as context
    async fn run_with_plan(&mut self, task: &str) -> Result<TurnResult, Box<dyn std::error::Error>> {
        // Phase 1: Think + Plan
        tracing::info!("Planmodus: Fase 1 — analyse og planlegging");
        let plan_prompt = format!(
            "Du skal løse denne oppgaven:\n\n{}\n\n\
             Før du handler, lag en steg-for-steg plan. For hvert steg:\n\
             - Beskriv hva som skal gjøres\n\
             - Hvilke filer som berøres\n\
             - Mulige risikoer\n\n\
             Bruk verktøy for å undersøke kodebasen (les filer, søk, etc.) \
             for å lage en informert plan. Ikke gjør endringer ennå.\n\n\
             Avslutt med en nummerert plan i dette formatet:\n\
             ## Plan\n\
             1. [steg]\n\
             2. [steg]\n\
             ...",
            task
        );

        self.messages.push(Message {
            role: "user".into(),
            content: Some(plan_prompt),
            tool_calls: None,
            tool_call_id: None,
        });

        // Let the LLM explore and create a plan (allows tool use for research)
        let plan_result = self.run_turn().await?;
        if !matches!(plan_result, TurnResult::Done) {
            return Ok(plan_result);
        }

        // Phase 2: Self-critique
        tracing::info!("Planmodus: Fase 2 — selvkritikk");
        self.messages.push(Message {
            role: "user".into(),
            content: Some(
                "Kritiser planen din. Vurder:\n\
                 - Er det steg som mangler?\n\
                 - Er rekkefølgen riktig?\n\
                 - Kan noe gå galt? Hva er fallback?\n\
                 - Er det enklere alternativer?\n\
                 - Berører endringene mer enn nødvendig?\n\n\
                 Juster planen om nødvendig, og presenter den endelige \
                 versjonen. Ikke implementer ennå."
                    .into(),
            ),
            tool_calls: None,
            tool_call_id: None,
        });

        let critique_result = self.run_turn().await?;
        if !matches!(critique_result, TurnResult::Done) {
            return Ok(critique_result);
        }

        // Phase 3: Execute
        tracing::info!("Planmodus: Fase 3 — utføring");
        self.messages.push(Message {
            role: "user".into(),
            content: Some(
                "Planen er godkjent. Implementer den nå, steg for steg. \
                 For hvert steg: utfør, verifiser at det fungerer, \
                 og gå videre til neste. Rapporter fremgang underveis."
                    .into(),
            ),
            tool_calls: None,
            tool_call_id: None,
        });

        self.run_turn().await
    }

    /// Decide whether to use plan mode based on task analysis.
    ///
    /// Heuristic: if the task mentions multiple files, complex operations,
    /// or design/architecture keywords, use plan mode.
    fn should_plan(task: &str) -> PlanDecision {
        let task_lower = task.to_lowercase();

        // Keywords that strongly suggest planning is needed
        let plan_keywords = [
            "implementer",
            "implement",
            "redesign",
            "refaktorer",
            "refactor",
            "migrer",
            "migrate",
            "arkitektur",
            "architecture",
            "bygg",
            "build",
            "lag en",
            "create a",
            "sett opp",
            "set up",
            "endre alle",
            "change all",
            "oppdater alle",
            "update all",
        ];

        // Keywords that suggest simple tasks (no plan needed)
        let simple_keywords = [
            "les ",
            "read ",
            "vis ",
            "show ",
            "hva er",
            "what is",
            "forklar",
            "explain",
            "sjekk",
            "check",
            "finn",
            "find",
            "søk",
            "search",
        ];

        // Simple tasks — direct execution
        for kw in &simple_keywords {
            if task_lower.starts_with(kw) {
                return PlanDecision::Direct;
            }
        }

        // Complex keywords — plan
        for kw in &plan_keywords {
            if task_lower.contains(kw) {
                return PlanDecision::Plan;
            }
        }

        // Length heuristic: long tasks are more likely to need planning
        if task.len() > 500 {
            return PlanDecision::Plan;
        }

        PlanDecision::Direct
    }

    /// Total input tokens across all models.
    fn total_input_tokens(&self) -> u64 {
        self.total_usage.values().map(|u| u.input_tokens).sum()
    }

    /// Total output tokens across all models.
    fn total_output_tokens(&self) -> u64 {
        self.total_usage.values().map(|u| u.output_tokens).sum()
    }

    /// Print token usage summary to stderr.
    fn print_summary(&self) {
        eprintln!("\n--- Token-forbruk ---");
        for (model, usage) in &self.total_usage {
            let model_cost = calculate_cost(model, usage);
            eprintln!(
                "  {}: {} inn / {} ut (${:.4})",
                model, usage.input_tokens, usage.output_tokens, model_cost
            );
        }
        eprintln!(
            "  Totalt: {} inn / {} ut",
            self.total_input_tokens(),
            self.total_output_tokens()
        );
        eprintln!("  Kostnad: ${:.4}", self.total_cost);
        if let Some(max_cost) = self.max_cost {
            eprintln!(
                "  Budsjett: ${:.2} ({:.0}% brukt)",
                max_cost,
                (self.total_cost / max_cost) * 100.0
            );
        }
        eprintln!("  Iterasjoner: {}", self.total_iterations);
    }

    /// Build a checkpoint from the current session state.
    fn build_checkpoint(&self, status: &str, summary: Option<String>) -> checkpoint::Checkpoint {
        checkpoint::Checkpoint {
            session_id: self.session_id.clone(),
            model_spec: self.model_spec.clone(),
            messages: self.messages.clone(),
            usage: self
                .total_usage
                .iter()
                .map(|(k, v)| (k.clone(), checkpoint::TokenUsageData::from(v)))
                .collect(),
            total_cost: self.total_cost,
            total_iterations: self.total_iterations,
            task: self.task_text.clone(),
            task_node_id: self.task_node_id.clone(),
            working_dir: self.working_dir.display().to_string(),
            files_changed: vec![], // Filled at session end
            timestamp: chrono::Utc::now(),
            started_at: self.started_at,
            status: status.into(),
            summary,
        }
    }

    /// Save a checkpoint if enough tool calls have elapsed.
    fn maybe_checkpoint(&mut self) {
        if self.checkpoint_interval == 0 {
            return;
        }
        self.tool_calls_since_checkpoint += 1;
        if self.tool_calls_since_checkpoint >= self.checkpoint_interval {
            self.tool_calls_since_checkpoint = 0;
            let cp = self.build_checkpoint("running", None);
            if let Err(e) = checkpoint::save(&cp) {
                tracing::warn!(error = %e, "Checkpoint-lagring feilet");
            }
        }
    }

    /// Save final checkpoint and log usage to PG.
    async fn finalize(&mut self, status: &str) {
        let files = checkpoint::changed_files(&self.working_dir).await;
        let summary = self
            .messages
            .iter()
            .rev()
            .find(|m| m.role == "assistant" && m.content.is_some())
            .and_then(|m| m.content.clone());

        let mut cp = self.build_checkpoint(status, summary);
        cp.files_changed = files;

        // Save checkpoint
        if let Err(e) = checkpoint::save(&cp) {
            tracing::warn!(error = %e, "Endelig checkpoint-lagring feilet");
        }

        // Log to PG
        if let Some(ref pool) = self.pg_pool {
            if let Err(e) = checkpoint::log_usage_to_pg(pool, &cp).await {
                tracing::warn!(error = %e, "Kostnadslogging til PG feilet");
            }
        }

        // Print session report
        let report = checkpoint::session_report(&cp);
        eprintln!("\n{}", report);

        // Cleanup old checkpoints (keep last 20)
        match checkpoint::cleanup(20) {
            Ok(0) => {}
            Ok(n) => tracing::debug!(removed = n, "Ryddet gamle checkpoints"),
            Err(e) => tracing::warn!(error = %e, "Checkpoint-opprydding feilet"),
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("synops_agent=info".parse().unwrap()),
        )
        .init();

    let cli = Cli::parse();

    // Handle daemon subcommand
    if let Some(Command::Daemon {
        interval,
        model,
        working_dir,
        heartbeat,
        pid_file,
        max_cost_per_task,
        max_iterations,
        verbose,
    }) = cli.command
    {
        let pool = synops_common::db::connect()
            .await
            .map_err(|e| format!("PG-tilkobling påkrevd for daemon: {e}"))?;

        let config = daemon::DaemonConfig {
            interval_secs: interval,
            default_model: model,
            working_dir,
            heartbeat_path: heartbeat,
            pid_path: pid_file,
            max_cost_per_task: Some(max_cost_per_task),
            max_iterations,
            verbose,
        };

        return daemon::run(pool, config).await.map_err(Into::into);
    }

    // Handle --resume: restore from checkpoint
    if let Some(ref resume_id) = cli.resume {
        let cp = if resume_id == "latest" {
            checkpoint::load_latest()
        } else {
            checkpoint::load(resume_id)
        };

        match cp {
            Ok(cp) => {
                return resume_session(cp, &cli).await;
            }
            Err(e) => {
                eprintln!("Feil ved gjenoppretting: {}", e);
                std::process::exit(1);
            }
        }
    }

    // Validate: need either --task or --interactive
    if cli.task.is_none() && !cli.interactive {
        eprintln!("Feil: Enten --task eller --interactive er påkrevd.");
        eprintln!("  synops-agent --task \"oppgaven\"");
        eprintln!("  synops-agent --interactive");
        std::process::exit(1);
    }

    // Spawn Claude Code direkte hvis --claude
    if cli.claude {
        let task = cli.task.as_deref().unwrap_or("interactive session");
        return spawn_claude_code(task, &cli.working_dir).await;
    }

    let api_keys = ApiKeys::from_env();
    let provider = create_provider(&cli.model, &api_keys, cli.max_tokens)?;

    tracing::info!(
        model = provider.model_id(),
        provider = provider.name(),
        "Starter agent"
    );

    let system_prompt = build_system_prompt(cli.system.as_deref(), &cli.working_dir).await;
    let compaction_config = CompactionConfig {
        context_window: provider.context_window(),
        ..Default::default()
    };

    // Connect to PG if DATABASE_URL is set (optional — tools degrade gracefully)
    let pg_pool = match synops_common::db::connect().await {
        Ok(pool) => {
            tracing::info!("PG tilkoblet (grafintegrasjon aktiv)");

            // Release stale tasks on startup
            match graph::release_stale_tasks(&pool).await {
                Ok(0) => {}
                Ok(n) => tracing::warn!(count = n, "Frigjorde stale tasks (>60 min)"),
                Err(e) => tracing::warn!(error = %e, "Krasj-deteksjon feilet"),
            }

            Some(pool)
        }
        Err(e) => {
            tracing::info!(reason = %e, "PG ikke tilgjengelig — synops_query deaktivert");
            None
        }
    };

    tracing::info!(
        context_window = compaction_config.context_window,
        max_cost = cli
            .max_cost
            .map(|c| format!("${:.2}", c))
            .as_deref()
            .unwrap_or("unlimited"),
        pg = pg_pool.is_some(),
        "Agent konfigurert"
    );

    // Initialize messages with system prompt
    let messages = vec![Message {
        role: "system".into(),
        content: Some(system_prompt),
        tool_calls: None,
        tool_call_id: None,
    }];

    let interrupted = Arc::new(AtomicBool::new(false));

    let mut session = AgentSession {
        provider,
        messages,
        tool_defs: tools::tool_definitions(),
        total_usage: HashMap::new(),
        total_cost: 0.0,
        total_iterations: 0,
        retry_config: RetryConfig {
            max_retries: cli.max_retries,
            ..Default::default()
        },
        compaction_config,
        working_dir: cli.working_dir.clone(),
        max_iterations: cli.max_iterations,
        max_cost: cli.max_cost,
        verbose: cli.verbose,
        interrupted: interrupted.clone(),
        pg_pool,
        session_id: checkpoint::new_session_id(),
        model_spec: cli.model.clone(),
        task_text: cli.task.clone(),
        task_node_id: None,
        checkpoint_interval: cli.checkpoint_interval,
        tool_calls_since_checkpoint: 0,
        started_at: chrono::Utc::now(),
    };

    // Git: branch-per-task
    if let Some(ref branch_name) = cli.branch {
        match git::create_branch(&cli.working_dir, branch_name).await {
            Ok(_) => tracing::info!(branch = branch_name.as_str(), "Opprettet og byttet til branch"),
            Err(e) => {
                // Branch might already exist — try checkout
                match git::checkout(&cli.working_dir, branch_name).await {
                    Ok(_) => tracing::info!(branch = branch_name.as_str(), "Byttet til eksisterende branch"),
                    Err(_) => {
                        tracing::error!(error = %e, "Kunne ikke opprette eller bytte til branch");
                        eprintln!("Feil: Kunne ikke opprette branch '{}': {}", branch_name, e);
                        std::process::exit(1);
                    }
                }
            }
        }
    }

    if cli.interactive {
        run_interactive(&mut session).await?;
    } else {
        // Batch mode
        let task = cli.task.unwrap();

        // Decide whether to use plan mode
        let use_plan = if cli.plan {
            true
        } else if cli.no_plan {
            false
        } else {
            AgentSession::should_plan(&task) == PlanDecision::Plan
        };

        let result = if use_plan {
            tracing::info!("Bruker planmodus (Extended ReAct Loop)");
            session.run_with_plan(&task).await?
        } else {
            // Direct execution
            session.messages.push(Message {
                role: "user".into(),
                content: Some(task.clone()),
                tool_calls: None,
                tool_call_id: None,
            });
            session.run_turn().await?
        };

        // Determine final status
        let status = match &result {
            TurnResult::Done => "completed",
            TurnResult::BudgetExhausted => "budget_exhausted",
            TurnResult::MaxIterations => "max_iterations",
            TurnResult::Interrupted => "interrupted",
        };

        // Finalize session: checkpoint, PG logging, report
        session.finalize(status).await;

        // Show diff after task
        if let Ok(diff_output) = git::diff(&cli.working_dir).await {
            if diff_output != "No changes" {
                eprintln!("\n--- Endringer ---");
                // Show stat instead of full diff for readability
                if let Ok(stat) = git::diff_stat(&cli.working_dir).await {
                    eprintln!("{}", stat.trim());
                }
            }
        }

        // Auto-commit after successful task
        let should_commit = !cli.no_commit && matches!(result, TurnResult::Done);
        if should_commit {
            let commit_msg = if let Some(ref msg) = cli.commit_msg {
                msg.clone()
            } else {
                // Auto-generate from diff
                git::auto_commit_message(&cli.working_dir)
                    .await
                    .unwrap_or_else(|_| format!("synops-agent: {}", task.chars().take(72).collect::<String>()))
            };

            match git::commit(&cli.working_dir, &commit_msg).await {
                Ok(output) => {
                    let output = output.trim();
                    if output == "Nothing to commit" {
                        tracing::info!("Ingen endringer å committe");
                    } else {
                        tracing::info!(message = commit_msg.as_str(), "Auto-commit");
                        eprintln!("\n--- Commit ---\n{}", output);

                        // Push if enabled
                        let should_push = cli.push && !cli.no_push;
                        if should_push {
                            match git::push(&cli.working_dir).await {
                                Ok(_) => tracing::info!("Pushet til remote"),
                                Err(e) => tracing::warn!(error = %e, "Push feilet"),
                            }
                        }
                    }
                }
                Err(e) => tracing::warn!(error = %e, "Commit feilet"),
            }
        }

        if matches!(result, TurnResult::BudgetExhausted) {
            eprintln!("\n⚠ Budsjettgrense nådd. Oppgaven er ikke fullført.");
            eprintln!("  Gjenoppta med: synops-agent --resume {}", session.session_id);
            std::process::exit(2);
        }
    }

    Ok(())
}

/// Interactive REPL mode.
async fn run_interactive(
    session: &mut AgentSession,
) -> Result<(), Box<dyn std::error::Error>> {
    use rustyline::error::ReadlineError;
    use rustyline::{DefaultEditor, Config, EditMode};

    let config = Config::builder()
        .edit_mode(EditMode::Emacs)
        .auto_add_history(true)
        .build();
    let mut rl = DefaultEditor::with_config(config)?;

    // Load history
    let history_path = dirs_history_path();
    let _ = rl.load_history(&history_path);

    let model_short = short_model_name(session.provider.model_id()).to_owned();

    eprintln!("synops-agent interaktiv modus");
    eprintln!("Modell: {}", session.provider.model_id());
    eprintln!("Ctrl+C avbryter pågående kall. Ctrl+D eller \"exit\" avslutter.");
    eprintln!();

    // Install Ctrl+C handler that sets the interrupted flag
    let interrupted = session.interrupted.clone();
    ctrlc::set_handler(move || {
        interrupted.store(true, Ordering::SeqCst);
    })
    .expect("Kunne ikke sette Ctrl+C handler");

    loop {
        let total_tokens = session.total_input_tokens() + session.total_output_tokens();
        let prompt = if total_tokens > 0 {
            format!("[{}T ${:.3}] {} > ", format_tokens(total_tokens), session.total_cost, model_short)
        } else {
            format!("{} > ", model_short)
        };

        // Reset interrupted flag before reading input
        session.interrupted.store(false, Ordering::SeqCst);

        let input = match rl.readline(&prompt) {
            Ok(line) => line,
            Err(ReadlineError::Interrupted) => {
                // Ctrl+C while waiting for input — just show new prompt
                eprintln!("^C");
                continue;
            }
            Err(ReadlineError::Eof) => {
                // Ctrl+D — exit
                eprintln!("\nAvslutter.");
                break;
            }
            Err(e) => {
                eprintln!("Readline-feil: {}", e);
                break;
            }
        };

        // Support multi-line: if input ends with '\', keep reading
        let input = if input.ends_with('\\') {
            let mut multi = input.trim_end_matches('\\').to_string();
            loop {
                match rl.readline("... ") {
                    Ok(line) => {
                        if line.ends_with('\\') {
                            multi.push('\n');
                            multi.push_str(line.trim_end_matches('\\'));
                        } else {
                            multi.push('\n');
                            multi.push_str(&line);
                            break;
                        }
                    }
                    Err(_) => break,
                }
            }
            multi
        } else {
            input
        };

        let trimmed = input.trim();
        if trimmed.is_empty() {
            continue;
        }

        // Built-in commands
        match trimmed {
            "exit" | "quit" | "/exit" | "/quit" => {
                eprintln!("Avslutter.");
                break;
            }
            "/stats" | "/tokens" => {
                session.print_summary();
                continue;
            }
            "/clear" => {
                // Keep system prompt, discard conversation
                session.messages.truncate(1);
                session.total_usage.clear();
                session.total_cost = 0.0;
                session.total_iterations = 0;
                eprintln!("Samtale nullstilt.");
                continue;
            }
            "/help" => {
                eprintln!("Kommandoer:");
                eprintln!("  /plan <oppgave> — Kjør med planmodus (tenk → plan → selvkritikk → utfør)");
                eprintln!("  /stats          — Vis token-forbruk og kostnad");
                eprintln!("  /clear          — Nullstill samtalen");
                eprintln!("  /help           — Vis denne hjelpen");
                eprintln!("  exit            — Avslutt");
                eprintln!();
                eprintln!("Multi-line: Avslutt linje med \\ for å fortsette.");
                eprintln!("Ctrl+C avbryter pågående LLM-kall/verktøy.");
                continue;
            }
            _ => {}
        }

        // Check for /plan command
        let (use_plan, effective_input) = if let Some(plan_task) = trimmed.strip_prefix("/plan ") {
            (true, plan_task.to_string())
        } else {
            (false, input.clone())
        };

        // Run with or without plan mode
        let turn_result = if use_plan {
            eprintln!("[planmodus aktivert]");
            session.run_with_plan(&effective_input).await
        } else {
            session.messages.push(Message {
                role: "user".into(),
                content: Some(effective_input),
                tool_calls: None,
                tool_call_id: None,
            });
            session.run_turn().await
        };

        match turn_result {
            Ok(TurnResult::Done) => {
                // Normal completion
            }
            Ok(TurnResult::Interrupted) => {
                eprintln!("\n[avbrutt]");
            }
            Ok(TurnResult::BudgetExhausted) => {
                eprintln!("\n⚠ Budsjettgrense nådd (${:.4})", session.total_cost);
                break;
            }
            Ok(TurnResult::MaxIterations) => {
                eprintln!(
                    "\n⚠ Maks iterasjoner ({}) nådd for denne turn.",
                    session.max_iterations
                );
            }
            Err(e) => {
                eprintln!("\nFeil: {}", e);
            }
        }
    }

    // Finalize session
    session.finalize("completed").await;

    // Save history
    let _ = rl.save_history(&history_path);

    Ok(())
}

/// Resume a session from a checkpoint.
async fn resume_session(
    cp: checkpoint::Checkpoint,
    cli: &Cli,
) -> Result<(), Box<dyn std::error::Error>> {
    eprintln!("Gjenopptar sesjon: {}", cp.session_id);
    eprintln!(
        "  Oppgave: {}",
        cp.task.as_deref().unwrap_or("(ukjent)")
    );
    eprintln!(
        "  Forrige status: {} ({} iterasjoner, ${:.4})",
        cp.status, cp.total_iterations, cp.total_cost
    );
    eprintln!("  Meldinger i historikk: {}", cp.messages.len());

    let api_keys = ApiKeys::from_env();
    let model_spec = &cli.model;
    let provider = create_provider(model_spec, &api_keys, cli.max_tokens)?;

    let compaction_config = CompactionConfig {
        context_window: provider.context_window(),
        ..Default::default()
    };

    // Restore token usage
    let total_usage: HashMap<String, TokenUsage> = cp
        .usage
        .iter()
        .map(|(k, v)| (k.clone(), TokenUsage::from(v)))
        .collect();

    let pg_pool = match synops_common::db::connect().await {
        Ok(pool) => Some(pool),
        Err(_) => None,
    };

    let interrupted = Arc::new(AtomicBool::new(false));

    let mut session = AgentSession {
        provider,
        messages: cp.messages,
        tool_defs: tools::tool_definitions(),
        total_usage,
        total_cost: cp.total_cost,
        total_iterations: cp.total_iterations,
        retry_config: RetryConfig {
            max_retries: cli.max_retries,
            ..Default::default()
        },
        compaction_config,
        working_dir: PathBuf::from(&cp.working_dir),
        max_iterations: cli.max_iterations,
        max_cost: cli.max_cost,
        verbose: cli.verbose,
        interrupted,
        pg_pool,
        session_id: cp.session_id,
        model_spec: model_spec.clone(),
        task_text: cp.task.clone(),
        task_node_id: cp.task_node_id,
        checkpoint_interval: cli.checkpoint_interval,
        tool_calls_since_checkpoint: 0,
        started_at: cp.started_at,
    };

    // Add a resume message so the LLM knows it's continuing
    session.messages.push(Message {
        role: "user".into(),
        content: Some(
            "Sesjonen ble gjenopptatt etter avbrudd. Fortsett der du slapp. \
             Hva har du gjort så langt, og hva gjenstår?"
                .into(),
        ),
        tool_calls: None,
        tool_call_id: None,
    });

    let result = session.run_turn().await?;

    let status = match &result {
        TurnResult::Done => "completed",
        TurnResult::BudgetExhausted => "budget_exhausted",
        TurnResult::MaxIterations => "max_iterations",
        TurnResult::Interrupted => "interrupted",
    };

    session.finalize(status).await;

    if matches!(result, TurnResult::BudgetExhausted) {
        eprintln!("\n⚠ Budsjettgrense nådd.");
        eprintln!("  Gjenoppta med: synops-agent --resume {}", session.session_id);
        std::process::exit(2);
    }

    Ok(())
}

/// Format token count for display (e.g. 1234 → "1.2k", 1234567 → "1.2M").
fn format_tokens(tokens: u64) -> String {
    if tokens >= 1_000_000 {
        format!("{:.1}M", tokens as f64 / 1_000_000.0)
    } else if tokens >= 1_000 {
        format!("{:.1}k", tokens as f64 / 1_000.0)
    } else {
        tokens.to_string()
    }
}

/// Shorten model name for prompt display.
fn short_model_name(model: &str) -> &str {
    // Take the last path segment: "anthropic/claude-sonnet-4" → "claude-sonnet-4"
    model.rsplit('/').next().unwrap_or(model)
}

/// Path for REPL history file.
fn dirs_history_path() -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    let dir = PathBuf::from(home).join(".synops");
    let _ = std::fs::create_dir_all(&dir);
    dir.join("agent_history.txt")
}

/// Build system prompt with context about the project and git state.
pub(crate) async fn build_system_prompt(custom: Option<&str>, working_dir: &Path) -> String {
    let mut prompt = String::new();

    prompt.push_str("Du er synops-agent, en autonom utviklerassistent for Synops-plattformen.\n");
    prompt.push_str("Du har tilgang til verktøy for å lese, skrive og redigere filer, ");
    prompt.push_str("kjøre shell-kommandoer, og søke i koden.\n\n");
    prompt.push_str("Arbeidsmappe: ");
    prompt.push_str(&working_dir.display().to_string());
    prompt.push_str("\n\n");

    // Try to read CLAUDE.md for project context
    let claude_md = working_dir.join("CLAUDE.md");
    if claude_md.exists() {
        if let Ok(content) = std::fs::read_to_string(&claude_md) {
            let truncated = if content.len() > 8000 {
                format!("{}...\n(truncated)", &content[..8000])
            } else {
                content
            };
            prompt.push_str("# Prosjektkontekst (fra CLAUDE.md)\n\n");
            prompt.push_str(&truncated);
            prompt.push_str("\n\n");
        }
    }

    if let Some(custom) = custom {
        prompt.push_str("# Tilleggsinstruksjoner\n\n");
        prompt.push_str(custom);
        prompt.push_str("\n\n");
    }

    // Git context
    if let Some(git_ctx) = git::gather_context(working_dir).await {
        prompt.push_str(&git_ctx);
        prompt.push('\n');
    }

    prompt.push_str("Regler:\n");
    prompt.push_str("- Les relevante filer før du endrer dem\n");
    prompt.push_str("- Gjør minimale, fokuserte endringer\n");
    prompt.push_str("- Test at kode kompilerer (cargo check, npm run build)\n");
    prompt.push_str("- Bruk git-verktøyet for å se log, blame, diff og status\n");
    prompt.push_str("- Commit og push når oppgaven er ferdig (med beskrivende melding)\n");

    prompt
}

/// Spawn Claude Code for heavy tasks (uses paid subscription).
pub(crate) async fn spawn_claude_code(
    task: &str,
    working_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    tracing::info!("Spawner Claude Code for oppgaven");

    let output = tokio::process::Command::new("claude")
        .arg("-p")
        .arg(task)
        .arg("--dangerously-skip-permissions")
        .current_dir(working_dir)
        .output()
        .await?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    if !stdout.is_empty() {
        println!("{}", stdout);
    }
    if !stderr.is_empty() {
        eprintln!("{}", stderr);
    }

    if !output.status.success() {
        eprintln!(
            "Claude Code avsluttet med kode: {}",
            output.status.code().unwrap_or(-1)
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_should_plan_complex_tasks() {
        assert_eq!(AgentSession::should_plan("implementer plan-modus i agenten"), PlanDecision::Plan);
        assert_eq!(AgentSession::should_plan("refaktorer hele auth-systemet"), PlanDecision::Plan);
        assert_eq!(AgentSession::should_plan("migrate database to new schema"), PlanDecision::Plan);
        assert_eq!(AgentSession::should_plan("lag en ny CLI-kommando for export"), PlanDecision::Plan);
        assert_eq!(AgentSession::should_plan("oppdater alle TODO-kommentarer i prosjektet"), PlanDecision::Plan);
    }

    #[test]
    fn test_should_plan_simple_tasks() {
        assert_eq!(AgentSession::should_plan("les filen src/main.rs"), PlanDecision::Direct);
        assert_eq!(AgentSession::should_plan("vis innholdet i config"), PlanDecision::Direct);
        assert_eq!(AgentSession::should_plan("hva er denne funksjonen?"), PlanDecision::Direct);
        assert_eq!(AgentSession::should_plan("finn alle .rs filer"), PlanDecision::Direct);
        assert_eq!(AgentSession::should_plan("sjekk om testen passerer"), PlanDecision::Direct);
    }

    #[test]
    fn test_should_plan_long_tasks() {
        let long_task = "x".repeat(600);
        assert_eq!(AgentSession::should_plan(&long_task), PlanDecision::Plan);
    }

    #[test]
    fn test_should_plan_ambiguous_defaults_to_direct() {
        assert_eq!(AgentSession::should_plan("fiks buggen i login"), PlanDecision::Direct);
    }
}
