use serde_json::{Map, Value};

use crate::model::{CompactLease, StagePlan};

#[derive(Debug, Clone)]
pub(crate) struct ReportReady {
    pub(crate) lease_id: String,
    pub(crate) task: String,
    pub(crate) report: String,
    pub(crate) worker_reasoning_effort: String,
    pub(crate) worker_model: Option<String>,
}

impl ReportReady {
    fn to_payload(&self) -> Map<String, Value> {
        let mut map = Map::new();
        map.insert("lease_id".to_string(), Value::String(self.lease_id.clone()));
        map.insert("task".to_string(), Value::String(self.task.clone()));
        map.insert("report".to_string(), Value::String(self.report.clone()));
        map.insert(
            "worker_reasoning_effort".to_string(),
            Value::String(self.worker_reasoning_effort.clone()),
        );
        if let Some(worker_model) = &self.worker_model {
            map.insert(
                "worker_model".to_string(),
                Value::String(worker_model.clone()),
            );
        }
        map
    }
}

#[derive(Debug, Clone)]
pub(crate) struct CleanupCandidate {
    pub(crate) lease_id: String,
    pub(crate) task: String,
}

impl CleanupCandidate {
    fn to_payload(&self) -> Map<String, Value> {
        let mut map = Map::new();
        map.insert("lease_id".to_string(), Value::String(self.lease_id.clone()));
        map.insert("task".to_string(), Value::String(self.task.clone()));
        map
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ReadyTask {
    pub(crate) id: String,
    pub(crate) spec: String,
    pub(crate) task: String,
    pub(crate) scope: Vec<String>,
    pub(crate) verify: String,
    pub(crate) worker_reasoning_effort: String,
    pub(crate) worker_model: Option<String>,
}

impl ReadyTask {
    fn to_payload(&self) -> Map<String, Value> {
        let mut map = Map::new();
        map.insert("task".to_string(), Value::String(self.task.clone()));
        map.insert(
            "scope".to_string(),
            Value::Array(self.scope.iter().cloned().map(Value::String).collect()),
        );
        map.insert("verify".to_string(), Value::String(self.verify.clone()));
        map.insert(
            "worker_reasoning_effort".to_string(),
            Value::String(self.worker_reasoning_effort.clone()),
        );
        if let Some(worker_model) = &self.worker_model {
            map.insert(
                "worker_model".to_string(),
                Value::String(worker_model.clone()),
            );
        }
        map
    }
}

#[derive(Debug, Clone)]
pub(crate) struct BlockedTask {
    pub(crate) task: String,
    pub(crate) reason: String,
}

impl BlockedTask {
    pub(crate) fn to_payload(&self) -> Map<String, Value> {
        let mut map = Map::new();
        map.insert("task".to_string(), Value::String(self.task.clone()));
        map.insert("reason".to_string(), Value::String(self.reason.clone()));
        map
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum Phase {
    Blocked,
    Cleanup,
    Dispatch,
    Done,
    Recover,
    Stage,
    Validate,
    Wait,
}

impl Phase {
    fn as_str(self) -> &'static str {
        match self {
            Phase::Blocked => "blocked",
            Phase::Cleanup => "cleanup",
            Phase::Dispatch => "dispatch",
            Phase::Done => "done",
            Phase::Recover => "recover",
            Phase::Stage => "stage",
            Phase::Validate => "validate",
            Phase::Wait => "wait",
        }
    }
}

pub(crate) struct NextInput {
    pub(crate) stale: Vec<CompactLease>,
    pub(crate) reports_ready: Vec<ReportReady>,
    pub(crate) active: Vec<CompactLease>,
    pub(crate) stage: Vec<StagePlan>,
    pub(crate) cleanup: Vec<CleanupCandidate>,
    pub(crate) ready: Vec<ReadyTask>,
    pub(crate) blocked: Vec<BlockedTask>,
    pub(crate) counts: Map<String, Value>,
    pub(crate) older_than: String,
    pub(crate) explain: bool,
}

pub(crate) struct NextDecision {
    pub(crate) phase: Phase,
    pub(crate) commands: Vec<Vec<String>>,
    pub(crate) details: Map<String, Value>,
}

impl NextDecision {
    pub(crate) fn to_payload(&self) -> Map<String, Value> {
        let mut map = Map::new();
        map.insert(
            "phase".to_string(),
            Value::String(self.phase.as_str().to_string()),
        );
        if self.commands.len() == 1 {
            map.insert(
                "cmd".to_string(),
                Value::Array(
                    self.commands[0]
                        .iter()
                        .cloned()
                        .map(Value::String)
                        .collect(),
                ),
            );
        } else if !self.commands.is_empty() {
            map.insert(
                "cmds".to_string(),
                Value::Array(
                    self.commands
                        .iter()
                        .map(|command| {
                            Value::Array(command.iter().cloned().map(Value::String).collect())
                        })
                        .collect(),
                ),
            );
        }
        map.extend(self.details.clone());
        map
    }
}

pub(crate) fn decide_next(input: NextInput) -> NextDecision {
    let NextInput {
        stale,
        reports_ready,
        active,
        stage,
        cleanup,
        ready,
        blocked,
        counts,
        older_than,
        explain,
    } = input;
    let has_blocked = !blocked.is_empty();
    let visible_blocked = if explain { blocked } else { Vec::new() };
    if !stale.is_empty() {
        let mut details = Map::new();
        details.insert("stale".to_string(), compact_array(stale));
        insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
        return NextDecision {
            phase: Phase::Recover,
            commands: vec![vec![
                "stale".to_string(),
                "--older-than".to_string(),
                older_than,
            ]],
            details,
        };
    }
    if !reports_ready.is_empty() {
        let first = &reports_ready[0];
        let report = first.report.clone();
        let lease_id = first.lease_id.clone();
        let mut details = Map::new();
        details.insert("reports_ready".to_string(), reports_array(reports_ready));
        insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
        return NextDecision {
            phase: Phase::Validate,
            commands: vec![
                vec!["report-check".to_string(), report],
                vec!["git-touched".to_string(), "--lease".to_string(), lease_id],
            ],
            details,
        };
    }
    if !active.is_empty() {
        let mut details = Map::new();
        details.insert("active".to_string(), compact_array(active));
        insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
        return NextDecision {
            phase: Phase::Wait,
            commands: Vec::new(),
            details,
        };
    }

    let stage_candidates: Vec<StagePlan> = stage
        .into_iter()
        .filter(|item| !item.pathspecs.is_empty() || !item.safe_to_stage)
        .collect();
    if !stage_candidates.is_empty() {
        let first = &stage_candidates[0];
        let mut details = Map::new();
        details.insert(
            "stage".to_string(),
            Value::Array(
                stage_candidates
                    .iter()
                    .map(|item| Value::Object(item.to_payload()))
                    .collect(),
            ),
        );
        insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
        return NextDecision {
            phase: Phase::Stage,
            commands: vec![vec![
                "git-stage-plan".to_string(),
                "--lease".to_string(),
                first.lease_id.clone(),
            ]],
            details,
        };
    }
    if !cleanup.is_empty() {
        let lease_id = cleanup[0].lease_id.clone();
        let mut details = Map::new();
        details.insert("cleanup".to_string(), cleanup_array(cleanup));
        insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
        return NextDecision {
            phase: Phase::Cleanup,
            commands: vec![vec!["close".to_string(), "--lease".to_string(), lease_id]],
            details,
        };
    }
    if !ready.is_empty() {
        let first = &ready[0];
        let spec = first.spec.clone();
        let id = first.id.clone();
        let mut details = Map::new();
        details.insert("ready".to_string(), ready_array(ready));
        insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
        return NextDecision {
            phase: Phase::Dispatch,
            commands: vec![vec![
                "lease".to_string(),
                spec,
                id,
                "--owner".to_string(),
                "worker:<agent-id>".to_string(),
            ]],
            details,
        };
    }

    let total: i64 = counts.values().filter_map(Value::as_i64).sum();
    let done = counts.get("done").and_then(Value::as_i64).unwrap_or(0);
    let phase = if total != 0 && done == total {
        Phase::Done
    } else if has_blocked {
        Phase::Blocked
    } else {
        Phase::Done
    };
    let mut details = Map::new();
    details.insert("counts".to_string(), Value::Object(counts));
    insert_non_empty(&mut details, "blocked", blocked_array(visible_blocked));
    NextDecision {
        phase,
        commands: Vec::new(),
        details,
    }
}

fn compact_array(items: Vec<CompactLease>) -> Value {
    Value::Array(
        items
            .into_iter()
            .map(|item| Value::Object(item.to_payload()))
            .collect(),
    )
}

fn reports_array(items: Vec<ReportReady>) -> Value {
    Value::Array(
        items
            .into_iter()
            .map(|item| Value::Object(item.to_payload()))
            .collect(),
    )
}

fn cleanup_array(items: Vec<CleanupCandidate>) -> Value {
    Value::Array(
        items
            .into_iter()
            .map(|item| Value::Object(item.to_payload()))
            .collect(),
    )
}

fn ready_array(items: Vec<ReadyTask>) -> Value {
    Value::Array(
        items
            .into_iter()
            .map(|item| Value::Object(item.to_payload()))
            .collect(),
    )
}

fn blocked_array(items: Vec<BlockedTask>) -> Value {
    Value::Array(
        items
            .into_iter()
            .map(|item| Value::Object(item.to_payload()))
            .collect(),
    )
}

fn insert_non_empty(map: &mut Map<String, Value>, key: &str, value: Value) {
    if value.as_array().is_some_and(Vec::is_empty) {
        return;
    }
    map.insert(key.to_string(), value);
}

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

    fn input() -> NextInput {
        NextInput {
            stale: Vec::new(),
            reports_ready: Vec::new(),
            active: Vec::new(),
            stage: Vec::new(),
            cleanup: Vec::new(),
            ready: Vec::new(),
            blocked: Vec::new(),
            counts: Map::new(),
            older_than: "30m".to_string(),
            explain: false,
        }
    }

    fn compact_lease(lease_id: &str) -> CompactLease {
        CompactLease {
            id: Value::String(lease_id.to_string()),
            task: Value::String("example/T001".to_string()),
            owner: Value::String("worker:agent_123".to_string()),
            kind: "task".to_string(),
            agent_id: None,
            worker_reasoning_effort: "medium".to_string(),
            worker_model: None,
            mode: "single".to_string(),
            age: 0,
            stale: false,
        }
    }

    fn report_ready() -> ReportReady {
        ReportReady {
            lease_id: "l_report".to_string(),
            task: "example/T001".to_string(),
            report: "reports/example/T001.md".to_string(),
            worker_reasoning_effort: "medium".to_string(),
            worker_model: None,
        }
    }

    fn cleanup_candidate() -> CleanupCandidate {
        CleanupCandidate {
            lease_id: "l_cleanup".to_string(),
            task: "example/T001".to_string(),
        }
    }

    fn ready_task() -> ReadyTask {
        ReadyTask {
            id: "T001".to_string(),
            spec: "example".to_string(),
            task: "example/T001".to_string(),
            scope: vec!["src/feature/".to_string()],
            verify: "validator".to_string(),
            worker_reasoning_effort: "medium".to_string(),
            worker_model: None,
        }
    }

    fn blocked_task() -> BlockedTask {
        BlockedTask {
            task: "example/T999".to_string(),
            reason: "status:blocked".to_string(),
        }
    }

    fn stage_plan(safe_to_stage: bool, pathspecs: Vec<&str>) -> StagePlan {
        StagePlan {
            lease_id: "l_stage".to_string(),
            task: "example/T001".to_string(),
            safe_to_stage,
            pathspecs: pathspecs.into_iter().map(str::to_string).collect(),
            records: Vec::new(),
            excluded: Map::new(),
            excluded_records: Map::new(),
        }
    }

    fn done_counts() -> Map<String, Value> {
        let mut map = Map::new();
        map.insert("done".to_string(), Value::Number(2.into()));
        map
    }

    #[test]
    fn next_phase_priority_is_table_driven() {
        type Arrange = Box<dyn Fn(&mut NextInput)>;
        let cases: Vec<(&str, Phase, Arrange)> = vec![
            (
                "stale/recover",
                Phase::Recover,
                Box::new(|input| {
                    input.stale = vec![compact_lease("l_stale")];
                    input.reports_ready = vec![report_ready()];
                    input.active = vec![compact_lease("l_active")];
                    input.stage = vec![stage_plan(true, vec!["src/planner.rs"])];
                    input.cleanup = vec![cleanup_candidate()];
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "reports_ready/validate",
                Phase::Validate,
                Box::new(|input| {
                    input.reports_ready = vec![report_ready()];
                    input.active = vec![compact_lease("l_active")];
                    input.stage = vec![stage_plan(true, vec!["src/planner.rs"])];
                    input.cleanup = vec![cleanup_candidate()];
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "active/wait",
                Phase::Wait,
                Box::new(|input| {
                    input.active = vec![compact_lease("l_active")];
                    input.stage = vec![stage_plan(true, vec!["src/planner.rs"])];
                    input.cleanup = vec![cleanup_candidate()];
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "unsafe stage/stage",
                Phase::Stage,
                Box::new(|input| {
                    input.stage = vec![stage_plan(false, Vec::new())];
                    input.cleanup = vec![cleanup_candidate()];
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "nonempty stage/stage",
                Phase::Stage,
                Box::new(|input| {
                    input.stage = vec![stage_plan(true, vec!["src/planner.rs"])];
                    input.cleanup = vec![cleanup_candidate()];
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "cleanup/cleanup",
                Phase::Cleanup,
                Box::new(|input| {
                    input.cleanup = vec![cleanup_candidate()];
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "ready/dispatch",
                Phase::Dispatch,
                Box::new(|input| {
                    input.ready = vec![ready_task()];
                    input.blocked = vec![blocked_task()];
                    input.counts = done_counts();
                }),
            ),
            (
                "blocked-only/blocked",
                Phase::Blocked,
                Box::new(|input| {
                    input.blocked = vec![blocked_task()];
                }),
            ),
            (
                "all-done/done",
                Phase::Done,
                Box::new(|input| {
                    input.counts = done_counts();
                }),
            ),
            ("empty scope/done", Phase::Done, Box::new(|_input| {})),
        ];

        for (name, expected_phase, arrange) in cases {
            let mut input = input();
            arrange(&mut input);

            let decision = decide_next(input);

            assert_eq!(decision.phase, expected_phase, "{name}");
        }
    }
}
