#![allow(dead_code)]

use std::fmt;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

use chrono::{DateTime, TimeDelta, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use crate::core::{now_iso, parse_duration, OrchError, OrchResult};
use crate::gitstate;
use crate::paths::{
    atomic_write, atomic_write_json, goal_current_path, goal_dir, path_to_string, read_text,
    repo_path,
};
use crate::taskfile::{quote_toml_string, split_frontmatter};

const GOAL_TOML: &str = "goal.toml";
const STATE_JSON: &str = "state.json";
const RESULTS_JSONL: &str = "results.jsonl";
const MEASUREMENTS_JSONL: &str = "measurements.jsonl";

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub(crate) struct GoalId(String);

impl GoalId {
    pub(crate) fn explicit(value: &str) -> OrchResult<Self> {
        if !value.is_empty()
            && !matches!(value, "." | "..")
            && value
                .bytes()
                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
        {
            return Ok(Self(value.to_string()));
        }
        Err(invalid_goal_id(value))
    }

    pub(crate) fn sanitize(value: &str) -> OrchResult<Self> {
        let raw = value.rsplit('/').next().unwrap_or(value);
        let mut out = String::new();
        let mut last_was_sep = true;
        for byte in raw.bytes() {
            if byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-') {
                out.push(byte as char);
                last_was_sep = false;
            } else if !last_was_sep {
                out.push('-');
                last_was_sep = true;
            }
        }
        while out.ends_with(['.', '_', '-']) {
            out.pop();
        }
        Self::explicit(&out)
    }

    pub(crate) fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for GoalId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
#[allow(dead_code)]
pub(crate) struct GoalRequest {
    pub(crate) id: Option<GoalId>,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct GoalInitRequest {
    pub(crate) goal_id: GoalId,
    pub(crate) goal: String,
    pub(crate) evaluator: String,
    pub(crate) primary_metric: String,
    pub(crate) direction: GoalDirection,
    pub(crate) minimum_delta: f64,
    pub(crate) max_iterations: u32,
    pub(crate) max_duration: TimeDelta,
    pub(crate) max_duration_raw: String,
    pub(crate) hypothesis: String,
    pub(crate) protected_surfaces: Vec<PathBuf>,
    pub(crate) scope: Vec<PathBuf>,
}

impl GoalInitRequest {
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn new(
        goal_id: GoalId,
        goal: impl Into<String>,
        evaluator: impl Into<String>,
        primary_metric: impl Into<String>,
        direction: GoalDirection,
        minimum_delta: f64,
        max_iterations: u32,
        max_duration: impl Into<String>,
        hypothesis: impl Into<String>,
        protected_surfaces: Vec<PathBuf>,
        scope: Vec<PathBuf>,
    ) -> OrchResult<Self> {
        let max_duration_raw = max_duration.into();
        let max_duration = parse_duration(&max_duration_raw)?;
        Ok(Self {
            goal_id,
            goal: goal.into(),
            evaluator: evaluator.into(),
            primary_metric: primary_metric.into(),
            direction,
            minimum_delta,
            max_iterations,
            max_duration,
            max_duration_raw,
            hypothesis: hypothesis.into(),
            protected_surfaces,
            scope,
        })
    }

    pub(crate) fn into_contract(self, started_at: String) -> GoalContract {
        GoalContract {
            goal_id: self.goal_id,
            goal: self.goal,
            evaluator: self.evaluator,
            primary_metric: self.primary_metric,
            direction: self.direction,
            minimum_delta: self.minimum_delta,
            max_iterations: self.max_iterations,
            max_duration: self.max_duration_raw,
            started_at,
            protected_surfaces: self.protected_surfaces,
            scope: self.scope,
        }
    }

    pub(crate) fn into_contract_and_state(self, started_at: String) -> (GoalContract, GoalState) {
        let hypothesis = self.hypothesis.clone();
        let contract = self.into_contract(started_at);
        let mut state = GoalState::initial(&contract);
        state.next_hypothesis = hypothesis;
        (contract, state)
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct GoalContract {
    pub(crate) goal_id: GoalId,
    pub(crate) goal: String,
    pub(crate) evaluator: String,
    pub(crate) primary_metric: String,
    pub(crate) direction: GoalDirection,
    pub(crate) minimum_delta: f64,
    pub(crate) max_iterations: u32,
    pub(crate) max_duration: String,
    pub(crate) started_at: String,
    pub(crate) protected_surfaces: Vec<PathBuf>,
    pub(crate) scope: Vec<PathBuf>,
}

impl GoalContract {
    pub(crate) fn write(&self, root: &Path) -> OrchResult<()> {
        let dir = safe_goal_dir(root, &self.goal_id)?;
        atomic_write(&dir.join(GOAL_TOML), &self.to_toml())?;
        atomic_write(&goal_current_path(root), &(self.goal_id.to_string() + "\n"))?;
        Ok(())
    }

    pub(crate) fn read(root: &Path, goal_id: &GoalId) -> OrchResult<Self> {
        let path = safe_goal_dir(root, goal_id)?.join(GOAL_TOML);
        toml::from_str(&read_text(&path)?).map_err(|err| {
            OrchError::new("invalid goal.toml")
                .detail("path", path_to_string(&path))
                .detail("message", err.to_string())
        })
    }

    fn to_toml(&self) -> String {
        let mut lines = vec![
            format!("goal_id = {}", quote_toml_string(self.goal_id.as_str())),
            format!("goal = {}", quote_toml_string(&self.goal)),
            format!("evaluator = {}", quote_toml_string(&self.evaluator)),
            String::new(),
            format!(
                "primary_metric = {}",
                quote_toml_string(&self.primary_metric)
            ),
            format!("direction = {}", quote_toml_string(self.direction.as_str())),
            format!("minimum_delta = {}", self.minimum_delta),
            String::new(),
            format!("max_iterations = {}", self.max_iterations),
            format!("max_duration = {}", quote_toml_string(&self.max_duration)),
            format!("started_at = {}", quote_toml_string(&self.started_at)),
            String::new(),
            format!(
                "protected_surfaces = {}",
                toml_string_array(&self.protected_surfaces)
            ),
            format!("scope = {}", toml_string_array(&self.scope)),
        ];
        lines.push(String::new());
        lines.join("\n")
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct GoalState {
    pub(crate) status: GoalStatus,
    pub(crate) cycle: String,
    pub(crate) started_at: String,
    pub(crate) updated_at: String,
    pub(crate) baseline_commit: Option<String>,
    pub(crate) baseline_value: Option<f64>,
    pub(crate) best_commit: Option<String>,
    pub(crate) best_value: Option<f64>,
    pub(crate) iterations_completed: u32,
    pub(crate) budget_exhausted: bool,
    pub(crate) budget_exhausted_reason: Option<String>,
    pub(crate) next_hypothesis: String,
    pub(crate) last_decision: Option<EvaluatorRecommendation>,
    pub(crate) last_report: Option<String>,
}

impl GoalState {
    pub(crate) fn initial(contract: &GoalContract) -> Self {
        let now = now_iso();
        Self {
            status: GoalStatus::Baseline,
            cycle: first_cycle_id(),
            started_at: contract.started_at.clone(),
            updated_at: now,
            baseline_commit: None,
            baseline_value: None,
            best_commit: None,
            best_value: None,
            iterations_completed: 0,
            budget_exhausted: false,
            budget_exhausted_reason: None,
            next_hypothesis: String::new(),
            last_decision: None,
            last_report: None,
        }
    }

    pub(crate) fn write(&self, root: &Path, goal_id: &GoalId) -> OrchResult<()> {
        atomic_write_json(&safe_goal_dir(root, goal_id)?.join(STATE_JSON), self)
    }

    pub(crate) fn read(root: &Path, goal_id: &GoalId) -> OrchResult<Self> {
        let path = safe_goal_dir(root, goal_id)?.join(STATE_JSON);
        serde_json::from_str(&read_text(&path)?).map_err(|err| {
            OrchError::new("invalid goal state")
                .detail("path", path_to_string(&path))
                .detail("message", err.to_string())
        })
    }

    pub(crate) fn budget_decision(
        &self,
        contract: &GoalContract,
        now: DateTime<Utc>,
    ) -> OrchResult<BudgetDecision> {
        if self.iterations_completed >= contract.max_iterations {
            return Ok(BudgetDecision::exhausted("max_iterations"));
        }
        let started_at = DateTime::parse_from_rfc3339(&contract.started_at)
            .map_err(|err| {
                OrchError::new("invalid goal started_at")
                    .detail("started_at", contract.started_at.clone())
                    .detail("message", err.to_string())
            })?
            .with_timezone(&Utc);
        if now - started_at >= parse_duration(&contract.max_duration)? {
            return Ok(BudgetDecision::exhausted("max_duration"));
        }
        Ok(BudgetDecision::continue_goal())
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct BudgetDecision {
    pub(crate) exhausted: bool,
    pub(crate) reason: Option<String>,
}

impl BudgetDecision {
    fn continue_goal() -> Self {
        Self {
            exhausted: false,
            reason: None,
        }
    }

    fn exhausted(reason: &str) -> Self {
        Self {
            exhausted: true,
            reason: Some(reason.to_string()),
        }
    }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum GoalStatus {
    Setup,
    Baseline,
    Ready,
    Running,
    Evaluate,
    Keep,
    Discard,
    Blocked,
    Done,
    Stopped,
}

impl GoalStatus {
    pub(crate) fn parse(value: &str) -> OrchResult<Self> {
        match value {
            "setup" => Ok(Self::Setup),
            "baseline" => Ok(Self::Baseline),
            "ready" => Ok(Self::Ready),
            "running" => Ok(Self::Running),
            "evaluate" => Ok(Self::Evaluate),
            "keep" => Ok(Self::Keep),
            "discard" => Ok(Self::Discard),
            "blocked" => Ok(Self::Blocked),
            "done" => Ok(Self::Done),
            "stopped" => Ok(Self::Stopped),
            _ => Err(OrchError::new("invalid goal status").detail("status", value)),
        }
    }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum GoalDirection {
    LowerIsBetter,
    HigherIsBetter,
    Target,
}

impl GoalDirection {
    pub(crate) fn parse(value: &str) -> OrchResult<Self> {
        match value {
            "lower-is-better" => Ok(Self::LowerIsBetter),
            "higher-is-better" => Ok(Self::HigherIsBetter),
            "target" => Ok(Self::Target),
            _ => Err(OrchError::new("invalid goal direction").detail("direction", value)),
        }
    }

    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::LowerIsBetter => "lower-is-better",
            Self::HigherIsBetter => "higher-is-better",
            Self::Target => "target",
        }
    }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum EvaluatorRecommendation {
    Keep,
    Discard,
    Blocked,
    Done,
}

impl EvaluatorRecommendation {
    pub(crate) fn parse(value: &str) -> OrchResult<Self> {
        match value {
            "keep" => Ok(Self::Keep),
            "discard" => Ok(Self::Discard),
            "blocked" => Ok(Self::Blocked),
            "done" => Ok(Self::Done),
            _ => {
                Err(OrchError::new("invalid evaluator recommendation")
                    .detail("recommendation", value))
            }
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct GoalReport {
    pub(crate) cycle: String,
    pub(crate) status: GoalReportStatus,
    pub(crate) next_hypothesis: String,
    pub(crate) body: String,
}

impl GoalReport {
    pub(crate) fn parse(text: &str, path: &Path) -> OrchResult<Self> {
        let (meta, body) = split_frontmatter(text, path)?;
        let cycle = required_string(&meta, "cycle")?;
        let status = GoalReportStatus::parse(&required_string(&meta, "status")?)?;
        let next_hypothesis = required_string(&meta, "next_hypothesis")?;
        Ok(Self {
            cycle,
            status,
            next_hypothesis,
            body,
        })
    }

    #[allow(dead_code)]
    pub(crate) fn read(path: &Path) -> OrchResult<Self> {
        Self::parse(&read_text(path)?, path)
    }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum GoalReportStatus {
    ReadyForEvaluation,
    Blocked,
}

impl GoalReportStatus {
    pub(crate) fn parse(value: &str) -> OrchResult<Self> {
        match value {
            "ready_for_evaluation" => Ok(Self::ReadyForEvaluation),
            "blocked" => Ok(Self::Blocked),
            _ => Err(OrchError::new("invalid goal report status").detail("status", value)),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EvaluatorResult {
    pub(crate) status: String,
    pub(crate) recommendation: EvaluatorRecommendation,
    pub(crate) metric: String,
    pub(crate) baseline: f64,
    pub(crate) candidate: f64,
    pub(crate) delta: f64,
    pub(crate) reason: String,
    pub(crate) raw: Map<String, Value>,
}

impl EvaluatorResult {
    pub(crate) fn parse_json(text: &str) -> OrchResult<Self> {
        let value: Value = serde_json::from_str(text).map_err(|err| {
            OrchError::new("invalid evaluator JSON").detail("message", err.to_string())
        })?;
        let Value::Object(raw) = value else {
            return Err(OrchError::new("invalid evaluator JSON").detail("message", "not an object"));
        };
        Ok(Self {
            status: required_string(&raw, "status")?,
            recommendation: EvaluatorRecommendation::parse(&required_string(
                &raw,
                "recommendation",
            )?)?,
            metric: required_string(&raw, "metric")?,
            baseline: required_f64(&raw, "baseline")?,
            candidate: required_f64(&raw, "candidate")?,
            delta: required_f64(&raw, "delta")?,
            reason: required_string(&raw, "reason")?,
            raw,
        })
    }

    pub(crate) fn append_measurement(
        &self,
        root: &Path,
        goal_id: &GoalId,
        cycle: &str,
    ) -> OrchResult<()> {
        let mut row = self.raw.clone();
        row.entry("cycle".to_string())
            .or_insert_with(|| Value::String(cycle.to_string()));
        append_jsonl(
            &safe_goal_dir(root, goal_id)?.join(MEASUREMENTS_JSONL),
            &row,
        )
    }
}

pub(crate) fn append_result(
    root: &Path,
    goal_id: &GoalId,
    result: &Map<String, Value>,
) -> OrchResult<()> {
    append_jsonl(&safe_goal_dir(root, goal_id)?.join(RESULTS_JSONL), result)
}

pub(crate) fn next_cycle_id(cycle: &str) -> OrchResult<String> {
    let raw = cycle
        .strip_prefix('C')
        .ok_or_else(|| OrchError::new("invalid goal cycle").detail("cycle", cycle))?;
    let number: u32 = raw
        .parse()
        .map_err(|_| OrchError::new("invalid goal cycle").detail("cycle", cycle))?;
    Ok(format!("C{:03}", number + 1))
}

pub(crate) fn first_cycle_id() -> String {
    "C001".to_string()
}

pub(crate) fn report_path(root: &Path, goal_id: &GoalId, cycle: &str) -> OrchResult<PathBuf> {
    repo_path(
        root,
        safe_goal_dir(root, goal_id)?
            .join("reports")
            .join(format!("{cycle}.md")),
        "goal_report_path",
    )
}

pub(crate) fn init_goal(root: &Path, request: GoalInitRequest) -> OrchResult<String> {
    let goal_id = request.goal_id.clone();
    let (contract, mut state) = request.into_contract_and_state(now_iso());
    match baseline_evaluator(root, &contract) {
        Ok((commit, value)) => {
            state.status = GoalStatus::Ready;
            state.baseline_commit = commit.clone();
            state.baseline_value = Some(value);
            state.best_commit = commit;
            state.best_value = Some(value);
        }
        Err(_) => {
            state.status = GoalStatus::Setup;
        }
    }
    state.updated_at = now_iso();
    contract.write(root)?;
    state.write(root, &goal_id)?;
    render_goal_prompt(root, &contract, &state)
}

pub(crate) fn current_goal(root: &Path) -> OrchResult<Option<(GoalContract, GoalState)>> {
    let current_path = goal_current_path(root);
    if !current_path.exists() {
        return Ok(None);
    }
    let raw = read_text(&current_path)?;
    let goal_id = GoalId::explicit(raw.trim())?;
    let contract = GoalContract::read(root, &goal_id)?;
    let state = GoalState::read(root, &goal_id)?;
    Ok(Some((contract, state)))
}

pub(crate) fn render_goal_prompt(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    match state.status {
        GoalStatus::Setup | GoalStatus::Baseline => maybe_establish_baseline(root, contract, state),
        GoalStatus::Ready | GoalStatus::Running | GoalStatus::Evaluate => {
            render_or_evaluate_goal(root, contract, state)
        }
        GoalStatus::Keep | GoalStatus::Discard => Ok(render_goal_decision(contract, state)),
        GoalStatus::Blocked => Ok(render_goal_blocked(contract, state)),
        GoalStatus::Done | GoalStatus::Stopped => render_goal_finish(root, contract, state),
    }
}

pub(crate) fn render_no_goal_prompt() -> String {
    "# Goal Setup\n\nNo current goal is initialized.\n\nRun `orchid goal init` with `--goal`, `--metric`, `--direction`, `--min-delta`, `--hypothesis`, `--max-iterations`, and `--max-duration`.\n".to_string()
}

pub(crate) fn render_goal_status(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let counts = goal_result_counts(root, &contract.goal_id)?;
    Ok(format!(
        "# Goal Status\n\n- Goal: `{}`\n- Reason: `{}`\n- Best result: `{}` at `{}`\n- Budget: `{}/{}` iterations, exhausted: `{}`\n- Kept cycles: `{}`\n- Discarded cycles: `{}`\n- Branch state: current goal files are local under `.orchid/goals/{}`\n\nNo pull request was created.\n",
        contract.goal_id.as_str(),
        goal_status_label(state.status),
        optional_f64(state.best_value),
        optional_string(state.best_commit.as_deref()),
        state.iterations_completed,
        contract.max_iterations,
        state.budget_exhausted,
        counts.kept,
        counts.discarded,
        contract.goal_id.as_str(),
    ))
}

pub(crate) fn finish_goal(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let mut next = state.clone();
    if !matches!(next.status, GoalStatus::Done) {
        next.status = GoalStatus::Stopped;
    }
    next.updated_at = now_iso();
    next.write(root, &contract.goal_id)?;
    render_goal_finish(root, contract, &next)
}

pub(crate) fn render_goal_finish(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let counts = goal_result_counts(root, &contract.goal_id)?;
    Ok(format!(
        "# Goal Finish\n\n- Goal: `{}`\n- Reason: `{}`\n- Best result: `{}` at `{}`\n- Budget: `{}/{}` iterations, exhausted: `{}`\n- Kept cycles: `{}`\n- Discarded cycles: `{}`\n- Branch state: finish requested without PR creation\n\nNo pull request was created.\n",
        contract.goal_id.as_str(),
        state
            .budget_exhausted_reason
            .as_deref()
            .unwrap_or_else(|| goal_status_label(state.status)),
        optional_f64(state.best_value),
        optional_string(state.best_commit.as_deref()),
        state.iterations_completed,
        contract.max_iterations,
        state.budget_exhausted,
        counts.kept,
        counts.discarded,
    ))
}

fn baseline_evaluator(root: &Path, contract: &GoalContract) -> OrchResult<(Option<String>, f64)> {
    let head = gitstate::head_commit(root)?;
    let output = evaluator_command(root, contract)?
        .env("ORCHID_GOAL_CYCLE", first_cycle_id())
        .env("ORCHID_GOAL_BASELINE_COMMIT", head.as_deref().unwrap_or(""))
        .env("ORCHID_GOAL_BASELINE_VALUE", "")
        .output()
        .map_err(OrchError::from)?;
    if !output.status.success() {
        return Err(OrchError::new("goal evaluator failed")
            .detail("evaluator", contract.evaluator.clone())
            .detail(
                "stderr",
                String::from_utf8_lossy(&output.stderr).trim().to_string(),
            ));
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    let result = EvaluatorResult::parse_json(&stdout)?;
    if result.metric != contract.primary_metric {
        return Err(OrchError::new("evaluator metric mismatch")
            .detail("expected", contract.primary_metric.clone())
            .detail("actual", result.metric));
    }
    Ok((head, result.baseline))
}

fn maybe_establish_baseline(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let Ok((commit, value)) = baseline_evaluator(root, contract) else {
        return Ok(render_goal_setup_for_contract(contract));
    };
    let mut next = state.clone();
    next.status = GoalStatus::Ready;
    next.baseline_commit = commit.clone();
    next.baseline_value = Some(value);
    next.best_commit = commit;
    next.best_value = Some(value);
    next.updated_at = now_iso();
    next.write(root, &contract.goal_id)?;
    render_goal_ready(root, contract, &next)
}

fn render_or_evaluate_goal(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let path = report_path(root, &contract.goal_id, &state.cycle)?;
    if !path.exists() {
        return if state.status == GoalStatus::Ready {
            render_goal_ready(root, contract, state)
        } else {
            render_goal_running(root, contract, state)
        };
    }
    let next = evaluate_cycle_report(root, contract, state, &path)?;
    render_goal_prompt(root, contract, &next)
}

fn evaluate_cycle_report(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
    path: &Path,
) -> OrchResult<GoalState> {
    let report = GoalReport::read(path)?;
    if report.cycle != state.cycle {
        return Err(OrchError::new("goal report cycle mismatch")
            .detail("expected", state.cycle.clone())
            .detail("actual", report.cycle));
    }

    let mut next = state.clone();
    next.status = GoalStatus::Evaluate;
    next.last_report = Some(path_to_string(path));
    next.next_hypothesis = report.next_hypothesis.clone();
    next.updated_at = now_iso();

    if report.status == GoalReportStatus::Blocked {
        next.status = GoalStatus::Blocked;
        next.budget_exhausted_reason = Some("report blocked".to_string());
        let candidate_commit = gitstate::head_commit(root)?;
        append_cycle_result(
            root,
            contract,
            CycleResultTrace {
                cycle: &next.cycle,
                baseline_commit: next.baseline_commit.as_deref(),
                candidate_commit: candidate_commit.as_deref(),
                next_hypothesis: &next.next_hypothesis,
                decision: "blocked",
                evaluator: None,
                reason: Some("report blocked"),
            },
        )?;
        next.write(root, &contract.goal_id)?;
        return Ok(next);
    }

    let protected_changes = changed_protected_surfaces(root, contract)?;
    if !protected_changes.is_empty() {
        next.status = GoalStatus::Blocked;
        next.budget_exhausted_reason = Some(format!(
            "protected surface changed: {}",
            protected_changes.join(", ")
        ));
        let candidate_commit = gitstate::head_commit(root)?;
        append_cycle_result(
            root,
            contract,
            CycleResultTrace {
                cycle: &next.cycle,
                baseline_commit: next.baseline_commit.as_deref(),
                candidate_commit: candidate_commit.as_deref(),
                next_hypothesis: &next.next_hypothesis,
                decision: "blocked",
                evaluator: None,
                reason: next.budget_exhausted_reason.as_deref(),
            },
        )?;
        next.write(root, &contract.goal_id)?;
        return Ok(next);
    }

    let evaluator = run_evaluator(root, contract, state)?;
    if evaluator.metric != contract.primary_metric {
        return Err(OrchError::new("evaluator metric mismatch")
            .detail("expected", contract.primary_metric.clone())
            .detail("actual", evaluator.metric));
    }
    evaluator.append_measurement(root, &contract.goal_id, &state.cycle)?;

    next.last_decision = Some(evaluator.recommendation);
    let recommendation = evaluator.recommendation;
    next.status = match recommendation {
        EvaluatorRecommendation::Keep => GoalStatus::Keep,
        EvaluatorRecommendation::Discard => GoalStatus::Discard,
        EvaluatorRecommendation::Blocked => GoalStatus::Blocked,
        EvaluatorRecommendation::Done => GoalStatus::Done,
    };
    next.updated_at = now_iso();
    let result_cycle = next.cycle.clone();
    let result_baseline_commit = next.baseline_commit.clone();
    let result_next_hypothesis = next.next_hypothesis.clone();
    let result_reason = evaluator.reason.clone();

    if recommendation == EvaluatorRecommendation::Keep {
        close_cycle(root, contract, &mut next, &evaluator)?;
        append_cycle_result(
            root,
            contract,
            CycleResultTrace {
                cycle: &result_cycle,
                baseline_commit: result_baseline_commit.as_deref(),
                candidate_commit: next.best_commit.as_deref(),
                next_hypothesis: &result_next_hypothesis,
                decision: recommendation_label(recommendation),
                evaluator: Some(&evaluator),
                reason: Some(&result_reason),
            },
        )?;
    } else {
        let candidate_commit = gitstate::head_commit(root)?;
        append_cycle_result(
            root,
            contract,
            CycleResultTrace {
                cycle: &result_cycle,
                baseline_commit: result_baseline_commit.as_deref(),
                candidate_commit: candidate_commit.as_deref(),
                next_hypothesis: &result_next_hypothesis,
                decision: recommendation_label(recommendation),
                evaluator: Some(&evaluator),
                reason: Some(&result_reason),
            },
        )?;
        close_cycle(root, contract, &mut next, &evaluator)?;
    }

    let budget = next.budget_decision(contract, crate::core::utc_now())?;
    if budget.exhausted && next.status != GoalStatus::Blocked {
        next.status = GoalStatus::Done;
        next.budget_exhausted = true;
        next.budget_exhausted_reason = budget.reason;
    } else if matches!(next.status, GoalStatus::Keep | GoalStatus::Discard) {
        next.status = GoalStatus::Ready;
    }
    next.write(root, &contract.goal_id)?;
    Ok(next)
}

fn run_evaluator(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<EvaluatorResult> {
    let output = evaluator_command(root, contract)?
        .env("ORCHID_GOAL_CYCLE", &state.cycle)
        .env(
            "ORCHID_GOAL_BASELINE_COMMIT",
            state.baseline_commit.as_deref().unwrap_or(""),
        )
        .env(
            "ORCHID_GOAL_BASELINE_VALUE",
            state
                .baseline_value
                .map(|value| value.to_string())
                .unwrap_or_default(),
        )
        .output()
        .map_err(OrchError::from)?;
    if !output.status.success() {
        return Err(OrchError::new("goal evaluator failed")
            .detail("evaluator", contract.evaluator.clone())
            .detail(
                "stderr",
                String::from_utf8_lossy(&output.stderr).trim().to_string(),
            ));
    }
    EvaluatorResult::parse_json(&String::from_utf8_lossy(&output.stdout))
}

fn evaluator_command(root: &Path, contract: &GoalContract) -> OrchResult<Command> {
    let goal_dir = safe_goal_dir(root, &contract.goal_id)?;
    let mut command = Command::new("sh");
    command
        .arg("-c")
        .arg(&contract.evaluator)
        .current_dir(root)
        .env("ORCHID_GOAL_ID", contract.goal_id.as_str())
        .env("ORCHID_GOAL_DIR", goal_dir);
    Ok(command)
}

fn changed_protected_surfaces(root: &Path, contract: &GoalContract) -> OrchResult<Vec<String>> {
    let scopes: Vec<String> = contract
        .protected_surfaces
        .iter()
        .map(|path| path_to_string(path))
        .collect();
    gitstate::changed_protected_paths(root, &scopes)
}

fn close_cycle(
    root: &Path,
    contract: &GoalContract,
    state: &mut GoalState,
    evaluator: &EvaluatorResult,
) -> OrchResult<()> {
    match evaluator.recommendation {
        EvaluatorRecommendation::Keep => keep_cycle(root, contract, state, evaluator),
        EvaluatorRecommendation::Discard => discard_cycle(root, state, evaluator),
        EvaluatorRecommendation::Blocked => {
            state.status = GoalStatus::Blocked;
            state.budget_exhausted_reason = Some(evaluator.reason.clone());
            Ok(())
        }
        EvaluatorRecommendation::Done => {
            state.status = GoalStatus::Done;
            state.iterations_completed = state.iterations_completed.saturating_add(1);
            Ok(())
        }
    }
}

fn keep_cycle(
    root: &Path,
    contract: &GoalContract,
    state: &mut GoalState,
    evaluator: &EvaluatorResult,
) -> OrchResult<()> {
    let _staged = gitstate::stage_goal_candidates(root)?;
    let commit = gitstate::commit_goal_keep(root, contract.goal_id.as_str(), &state.cycle)?;
    state.baseline_commit = Some(commit.clone());
    state.baseline_value = Some(evaluator.candidate);
    state.best_commit = Some(commit);
    state.best_value = Some(evaluator.candidate);
    state.iterations_completed = state.iterations_completed.saturating_add(1);
    state.cycle = next_cycle_id(&state.cycle)?;
    Ok(())
}

fn discard_cycle(
    root: &Path,
    state: &mut GoalState,
    _evaluator: &EvaluatorResult,
) -> OrchResult<()> {
    let baseline = state
        .baseline_commit
        .clone()
        .ok_or_else(|| OrchError::new("goal baseline commit missing"))?;
    gitstate::reset_hard(root, &baseline)?;
    gitstate::clean_goal_candidates(root)?;
    state.iterations_completed = state.iterations_completed.saturating_add(1);
    state.cycle = next_cycle_id(&state.cycle)?;
    Ok(())
}

struct CycleResultTrace<'a> {
    cycle: &'a str,
    baseline_commit: Option<&'a str>,
    candidate_commit: Option<&'a str>,
    next_hypothesis: &'a str,
    decision: &'a str,
    evaluator: Option<&'a EvaluatorResult>,
    reason: Option<&'a str>,
}

fn append_cycle_result(
    root: &Path,
    contract: &GoalContract,
    trace: CycleResultTrace<'_>,
) -> OrchResult<()> {
    let mut row = Map::new();
    row.insert("cycle".to_string(), Value::String(trace.cycle.to_string()));
    row.insert(
        "decision".to_string(),
        Value::String(trace.decision.to_string()),
    );
    row.insert(
        "baseline_commit".to_string(),
        trace
            .baseline_commit
            .map(|value| Value::String(value.to_string()))
            .unwrap_or(Value::Null),
    );
    row.insert(
        "candidate_commit".to_string(),
        trace
            .candidate_commit
            .map(|value| Value::String(value.to_string()))
            .unwrap_or(Value::Null),
    );
    row.insert(
        "next_hypothesis".to_string(),
        Value::String(trace.next_hypothesis.to_string()),
    );
    if let Some(reason) = trace.reason {
        row.insert("reason".to_string(), Value::String(reason.to_string()));
    }
    if let Some(evaluator) = trace.evaluator {
        row.insert(
            "metric".to_string(),
            Value::String(evaluator.metric.clone()),
        );
        row.insert("baseline".to_string(), json_number(evaluator.baseline)?);
        row.insert("candidate".to_string(), json_number(evaluator.candidate)?);
        row.insert("delta".to_string(), json_number(evaluator.delta)?);
    }
    append_result(root, &contract.goal_id, &row)
}

fn json_number(value: f64) -> OrchResult<Value> {
    serde_json::Number::from_f64(value)
        .map(Value::Number)
        .ok_or_else(|| OrchError::new("invalid JSON").detail("message", "non-finite number"))
}

fn recommendation_label(recommendation: EvaluatorRecommendation) -> &'static str {
    match recommendation {
        EvaluatorRecommendation::Keep => "keep",
        EvaluatorRecommendation::Discard => "discard",
        EvaluatorRecommendation::Blocked => "blocked",
        EvaluatorRecommendation::Done => "done",
    }
}

fn render_goal_setup_for_contract(contract: &GoalContract) -> String {
    format!(
        "# Goal Setup\n\n- Goal: `{}`\n- Evaluator: `{}`\n- Metric: `{}`\n\nMake `{}` run successfully from the repository root and print valid evaluator JSON with `status`, `recommendation`, `metric`, `baseline`, `candidate`, `delta`, and `reason` fields.\n\nAfter the evaluator works, run `orchid goal init` again with the same flags or run `orchid goal` to continue from the current state.\n",
        contract.goal_id.as_str(),
        contract.evaluator,
        contract.primary_metric,
        contract.evaluator,
    )
}

fn render_goal_ready(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let report_path = report_path(root, &contract.goal_id, &state.cycle)?;
    Ok(format!(
        "# Goal Ready\n\n- Goal: `{}`\n- Cycle: `{}`\n- Metric: `{}`\n- Direction: `{}`\n- Baseline: `{}` at `{}`\n- Budget: `{}/{}` iterations, max duration `{}`\n- Next hypothesis: {}\n- Recent results: `{}`\n- Report path: `{}`\n\nImplement one focused attempt, then write a Markdown report at the report path with TOML frontmatter containing `cycle`, `status`, and `next_hypothesis`.\n",
        contract.goal_id.as_str(),
        state.cycle,
        contract.primary_metric,
        contract.direction.as_str(),
        optional_f64(state.baseline_value),
        optional_string(state.baseline_commit.as_deref()),
        state.iterations_completed,
        contract.max_iterations,
        contract.max_duration,
        state.next_hypothesis,
        optional_string(state.last_report.as_deref()),
        path_to_string(&report_path),
    ))
}

fn render_goal_running(
    root: &Path,
    contract: &GoalContract,
    state: &GoalState,
) -> OrchResult<String> {
    let report_path = report_path(root, &contract.goal_id, &state.cycle)?;
    Ok(format!(
        "# Goal Running\n\n- Goal: `{}`\n- Cycle: `{}`\n- Expected report path: `{}`\n\nWrite the cycle report before asking Orchid to evaluate this attempt.\n",
        contract.goal_id.as_str(),
        state.cycle,
        path_to_string(&report_path),
    ))
}

fn render_goal_decision(contract: &GoalContract, state: &GoalState) -> String {
    format!(
        "# Goal Decision\n\n- Goal: `{}`\n- Cycle: `{}`\n- Decision: `{}`\n- Next hypothesis: {}\n\nOrchid recorded the decision and durable traces. Git keep/discard effects are pending.\n",
        contract.goal_id.as_str(),
        state.cycle,
        goal_status_label(state.status),
        state.next_hypothesis,
    )
}

fn render_goal_blocked(contract: &GoalContract, state: &GoalState) -> String {
    format!(
        "# Goal Blocked\n\n- Goal: `{}`\n- Cycle: `{}`\n- Reason: `{}`\n",
        contract.goal_id.as_str(),
        state.cycle,
        state
            .budget_exhausted_reason
            .as_deref()
            .unwrap_or("blocked"),
    )
}

#[derive(Default)]
struct GoalResultCounts {
    kept: usize,
    discarded: usize,
}

fn goal_result_counts(root: &Path, goal_id: &GoalId) -> OrchResult<GoalResultCounts> {
    let path = safe_goal_dir(root, goal_id)?.join(RESULTS_JSONL);
    if !path.exists() {
        return Ok(GoalResultCounts::default());
    }
    let mut counts = GoalResultCounts::default();
    for (index, line) in read_text(&path)?.lines().enumerate() {
        if line.trim().is_empty() {
            continue;
        }
        let value: Value = serde_json::from_str(line).map_err(|err| {
            OrchError::new("invalid goal result trace")
                .detail("path", path_to_string(&path))
                .detail("line", (index + 1).to_string())
                .detail("message", err.to_string())
        })?;
        match value.get("decision").and_then(Value::as_str) {
            Some("keep") => counts.kept += 1,
            Some("discard") => counts.discarded += 1,
            _ => {}
        }
    }
    Ok(counts)
}

fn goal_status_label(status: GoalStatus) -> &'static str {
    match status {
        GoalStatus::Setup => "setup",
        GoalStatus::Baseline => "baseline",
        GoalStatus::Ready => "ready",
        GoalStatus::Running => "running",
        GoalStatus::Evaluate => "evaluate",
        GoalStatus::Keep => "keep",
        GoalStatus::Discard => "discard",
        GoalStatus::Blocked => "blocked",
        GoalStatus::Done => "done",
        GoalStatus::Stopped => "stopped",
    }
}

fn optional_f64(value: Option<f64>) -> String {
    value
        .map(|value| value.to_string())
        .unwrap_or_else(|| "pending".to_string())
}

fn optional_string(value: Option<&str>) -> &str {
    value.unwrap_or("pending")
}

fn safe_goal_dir(root: &Path, goal_id: &GoalId) -> OrchResult<PathBuf> {
    let goals = repo_path(root, crate::paths::goals_dir(root), "goals_dir")?;
    let dir = repo_path(root, goal_dir(root, goal_id.as_str()), "goal_dir")?;
    if !dir.starts_with(&goals) || dir == goals {
        return Err(invalid_goal_id(goal_id.as_str()));
    }
    Ok(dir)
}

fn append_jsonl(path: &Path, data: &Map<String, Value>) -> OrchResult<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let mut file = OpenOptions::new().create(true).append(true).open(path)?;
    serde_json::to_writer(&mut file, data)
        .map_err(|err| OrchError::new("invalid JSON").detail("message", err.to_string()))?;
    file.write_all(b"\n")?;
    file.sync_all()?;
    Ok(())
}

fn required_string(map: &Map<String, Value>, key: &str) -> OrchResult<String> {
    match map.get(key).and_then(Value::as_str) {
        Some(value) if !value.is_empty() => Ok(value.to_string()),
        _ => Err(OrchError::new("missing required goal field").detail("field", key)),
    }
}

fn required_f64(map: &Map<String, Value>, key: &str) -> OrchResult<f64> {
    match map.get(key).and_then(Value::as_f64) {
        Some(value) => Ok(value),
        None => Err(OrchError::new("missing required goal field").detail("field", key)),
    }
}

fn toml_string_array(paths: &[PathBuf]) -> String {
    let values: Vec<String> = paths
        .iter()
        .map(|path| quote_toml_string(&path_to_string(path)))
        .collect();
    format!("[{}]", values.join(", "))
}

fn invalid_goal_id(value: &str) -> OrchError {
    OrchError::with_code("invalid goal id", "invalid_goal_id").detail("goal_id", value)
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;
    use serde_json::json;
    use tempfile::TempDir;

    #[test]
    fn explicit_goal_ids_accept_only_safe_ascii() {
        assert_eq!(
            GoalId::explicit("search.v2_ok-1").unwrap().as_str(),
            "search.v2_ok-1"
        );
        assert_eq!(GoalId::explicit("").unwrap_err().code, "invalid_goal_id");
        assert_eq!(GoalId::explicit("..").unwrap_err().code, "invalid_goal_id");
        assert_eq!(
            GoalId::explicit("../nope").unwrap_err().code,
            "invalid_goal_id"
        );
        assert_eq!(
            GoalId::explicit("ümlaut").unwrap_err().code,
            "invalid_goal_id"
        );
    }

    #[test]
    fn branch_goal_ids_sanitize_to_branch_leaf() {
        assert_eq!(
            GoalId::sanitize("loop/search-ranking-proof")
                .unwrap()
                .as_str(),
            "search-ranking-proof"
        );
        assert_eq!(
            GoalId::sanitize("feature/Search Ranking!")
                .unwrap()
                .as_str(),
            "Search-Ranking"
        );
        assert_eq!(GoalId::sanitize("///").unwrap_err().code, "invalid_goal_id");
    }

    #[test]
    fn parses_goal_direction_recommendation_and_status() {
        assert_eq!(
            GoalDirection::parse("lower-is-better").unwrap(),
            GoalDirection::LowerIsBetter
        );
        assert_eq!(
            EvaluatorRecommendation::parse("discard").unwrap(),
            EvaluatorRecommendation::Discard
        );
        assert_eq!(GoalStatus::parse("blocked").unwrap(), GoalStatus::Blocked);
        assert!(GoalDirection::parse("faster").is_err());
        assert!(EvaluatorRecommendation::parse("maybe").is_err());
        assert!(GoalStatus::parse("waiting").is_err());
    }

    #[test]
    fn parses_goal_report_frontmatter() {
        let text = r#"+++
cycle = "C001"
status = "ready_for_evaluation"
next_hypothesis = "precompute weights"
+++

## Summary
Done.
"#;
        let report = GoalReport::parse(text, Path::new("C001.md")).unwrap();
        assert_eq!(report.cycle, "C001");
        assert_eq!(report.status, GoalReportStatus::ReadyForEvaluation);
        assert_eq!(report.next_hypothesis, "precompute weights");
        assert!(report.body.contains("Done."));
    }

    #[test]
    fn rejects_invalid_goal_report_status() {
        let text = r#"+++
cycle = "C001"
status = "done"
next_hypothesis = "next"
+++
"#;
        assert!(GoalReport::parse(text, Path::new("C001.md")).is_err());
    }

    #[test]
    fn parses_evaluator_json_and_preserves_extra_fields() {
        let result = EvaluatorResult::parse_json(
            r#"{
              "status": "pass",
              "recommendation": "keep",
              "metric": "p95_ms",
              "baseline": 120.0,
              "candidate": 113.6,
              "delta": 6.4,
              "reason": "improved",
              "sample_count": 12
            }"#,
        )
        .unwrap();
        assert_eq!(result.recommendation, EvaluatorRecommendation::Keep);
        assert_eq!(result.metric, "p95_ms");
        assert_eq!(result.raw["sample_count"], 12);
        assert!(EvaluatorResult::parse_json(r#"{"recommendation":"keep"}"#).is_err());
    }

    #[test]
    fn budget_decision_checks_iteration_and_duration_limits() {
        let contract = contract();
        let mut state = GoalState::initial(&contract);
        let now = Utc.with_ymd_and_hms(2026, 6, 17, 12, 30, 0).unwrap();
        assert!(!state.budget_decision(&contract, now).unwrap().exhausted);

        state.iterations_completed = 10;
        let decision = state.budget_decision(&contract, now).unwrap();
        assert!(decision.exhausted);
        assert_eq!(decision.reason.as_deref(), Some("max_iterations"));

        state.iterations_completed = 9;
        let late = Utc.with_ymd_and_hms(2026, 6, 17, 23, 0, 1).unwrap();
        let decision = state.budget_decision(&contract, late).unwrap();
        assert!(decision.exhausted);
        assert_eq!(decision.reason.as_deref(), Some("max_duration"));
    }

    #[test]
    fn init_request_preserves_initial_hypothesis_in_state() {
        let (contract, state) = GoalInitRequest::new(
            GoalId::explicit("search-ranking-proof").unwrap(),
            "Reduce search ranking p95",
            "just goal-eval",
            "p95_ms",
            GoalDirection::LowerIsBetter,
            5.0,
            10,
            "10h",
            "cache normalized query features",
            vec![],
            vec![],
        )
        .unwrap()
        .into_contract_and_state("2026-06-17T12:00:00Z".to_string());

        assert_eq!(contract.goal_id.as_str(), "search-ranking-proof");
        assert_eq!(state.next_hypothesis, "cache normalized query features");
    }

    #[test]
    fn increments_cycle_ids() {
        assert_eq!(first_cycle_id(), "C001");
        assert_eq!(next_cycle_id("C001").unwrap(), "C002");
        assert_eq!(next_cycle_id("C099").unwrap(), "C100");
        assert!(next_cycle_id("1").is_err());
    }

    #[test]
    fn writes_goal_contract_state_pointer_and_jsonl_traces() {
        let tmp = TempDir::new().unwrap();
        std::fs::create_dir(tmp.path().join(".orchid")).unwrap();
        let contract = contract();
        contract.write(tmp.path()).unwrap();
        let state = GoalState::initial(&contract);
        state.write(tmp.path(), &contract.goal_id).unwrap();

        let goal_root = tmp.path().join(".orchid/goals/search-ranking-proof");
        assert!(goal_root.join(GOAL_TOML).exists());
        assert!(goal_root.join(STATE_JSON).exists());
        assert_eq!(
            std::fs::read_to_string(tmp.path().join(".orchid/goal-current")).unwrap(),
            "search-ranking-proof\n"
        );
        assert_eq!(
            GoalContract::read(tmp.path(), &contract.goal_id)
                .unwrap()
                .primary_metric,
            "p95_ms"
        );
        assert_eq!(
            GoalState::read(tmp.path(), &contract.goal_id)
                .unwrap()
                .status,
            GoalStatus::Baseline
        );

        let evaluator = EvaluatorResult::parse_json(
            r#"{"status":"pass","recommendation":"keep","metric":"p95_ms","baseline":120.0,"candidate":110.0,"delta":10.0,"reason":"better"}"#,
        )
        .unwrap();
        evaluator
            .append_measurement(tmp.path(), &contract.goal_id, "C001")
            .unwrap();
        append_result(
            tmp.path(),
            &contract.goal_id,
            json!({"cycle": "C001", "decision": "keep"})
                .as_object()
                .unwrap(),
        )
        .unwrap();
        assert!(std::fs::read_to_string(goal_root.join(MEASUREMENTS_JSONL))
            .unwrap()
            .contains("\"candidate\":110.0"));
        assert!(std::fs::read_to_string(goal_root.join(RESULTS_JSONL))
            .unwrap()
            .contains("\"decision\":\"keep\""));
    }

    #[test]
    fn report_paths_stay_under_goal_directory() {
        let tmp = TempDir::new().unwrap();
        std::fs::create_dir(tmp.path().join(".orchid")).unwrap();
        let id = GoalId::explicit("safe").unwrap();
        let path = report_path(tmp.path(), &id, "C001").unwrap();
        assert!(path.ends_with(".orchid/goals/safe/reports/C001.md"));
    }

    fn contract() -> GoalContract {
        GoalInitRequest::new(
            GoalId::explicit("search-ranking-proof").unwrap(),
            "Reduce search ranking p95",
            "just goal-eval",
            "p95_ms",
            GoalDirection::LowerIsBetter,
            5.0,
            10,
            "10h",
            "cache normalized query features",
            vec![PathBuf::from("justfile")],
            vec![],
        )
        .unwrap()
        .into_contract("2026-06-17T12:00:00Z".to_string())
    }
}
