use anyhow::Result;
use serde::Serialize;
use std::path::Path;
use workgraph::query::{ProjectSummary, project_summary, tasks_within_budget, tasks_within_hours};

/// JSON output for plan command
#[derive(Debug, Serialize)]
pub struct PlanOutput {
    pub summary: ProjectSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub budget_plan: Option<BudgetPlanOutput>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hours_plan: Option<HoursPlanOutput>,
}

#[derive(Debug, Serialize)]
pub struct BudgetPlanOutput {
    pub budget: f64,
    pub fits: Vec<TaskPlanItem>,
    pub exceeds: Vec<TaskPlanItem>,
    pub remaining: f64,
}

#[derive(Debug, Serialize)]
pub struct HoursPlanOutput {
    pub hours: f64,
    pub fits: Vec<TaskPlanItem>,
    pub exceeds: Vec<TaskPlanItem>,
    pub remaining: f64,
}

#[derive(Debug, Serialize)]
pub struct TaskPlanItem {
    pub id: String,
    pub title: String,
    pub cost: f64,
    pub hours: f64,
    pub is_ready: bool,
}

pub fn run(dir: &Path, budget: Option<f64>, hours: Option<f64>, json: bool) -> Result<()> {
    let (graph, _path) = super::load_workgraph(dir)?;
    let summary = project_summary(&graph);

    let budget_plan = budget.map(|b| {
        let result = tasks_within_budget(&graph, b);
        BudgetPlanOutput {
            budget: b,
            fits: result
                .fits
                .iter()
                .map(|t| TaskPlanItem {
                    id: t.id.to_string(),
                    title: t.title.to_string(),
                    cost: t.cost,
                    hours: t.hours,
                    is_ready: t.is_ready,
                })
                .collect(),
            exceeds: result
                .exceeds
                .iter()
                .map(|t| TaskPlanItem {
                    id: t.id.to_string(),
                    title: t.title.to_string(),
                    cost: t.cost,
                    hours: t.hours,
                    is_ready: t.is_ready,
                })
                .collect(),
            remaining: result.remaining,
        }
    });

    let hours_plan = hours.map(|h| {
        let result = tasks_within_hours(&graph, h);
        HoursPlanOutput {
            hours: h,
            fits: result
                .fits
                .iter()
                .map(|t| TaskPlanItem {
                    id: t.id.to_string(),
                    title: t.title.to_string(),
                    cost: t.cost,
                    hours: t.hours,
                    is_ready: t.is_ready,
                })
                .collect(),
            exceeds: result
                .exceeds
                .iter()
                .map(|t| TaskPlanItem {
                    id: t.id.to_string(),
                    title: t.title.to_string(),
                    cost: t.cost,
                    hours: t.hours,
                    is_ready: t.is_ready,
                })
                .collect(),
            remaining: result.remaining,
        }
    });

    if json {
        let output = PlanOutput {
            summary,
            budget_plan,
            hours_plan,
        };
        println!("{}", serde_json::to_string_pretty(&output)?);
    } else {
        print_human_output(&summary, budget_plan.as_ref(), hours_plan.as_ref());
    }

    Ok(())
}

fn print_human_output(
    summary: &ProjectSummary,
    budget_plan: Option<&BudgetPlanOutput>,
    hours_plan: Option<&HoursPlanOutput>,
) {
    // If no specific plan requested, show project overview
    if budget_plan.is_none() && hours_plan.is_none() {
        println!("Project Status:");
        println!(
            "  Tasks: {} open, {} done, {} in-progress",
            summary.open, summary.done, summary.in_progress
        );
        println!(
            "  Estimated: ${:.0} / {:.0} hours remaining",
            summary.total_cost, summary.total_hours
        );
        println!("  Ready: {} tasks", summary.ready);
        println!("  Blocked: {} tasks", summary.blocked);
        return;
    }

    // Budget-based planning
    if let Some(plan) = budget_plan {
        println!("With budget of ${:.0}, you can complete:", plan.budget);
        println!();

        if plan.fits.is_empty() {
            println!("  No tasks fit within this budget.");
        } else {
            for task in &plan.fits {
                let status = if task.is_ready { "ready" } else { "unblocked" };
                println!(
                    "  [x] {} (${:.0}, {:.0}h) - {}",
                    task.id, task.cost, task.hours, status
                );
            }
            println!("  Remaining: ${:.0}", plan.remaining);
        }

        if !plan.exceeds.is_empty() {
            println!();
            println!("Cannot fit:");
            for task in &plan.exceeds {
                println!(
                    "  [ ] {} (${:.0}, {:.0}h) - exceeds remaining budget",
                    task.id, task.cost, task.hours
                );
            }
        }
    }

    // Hours-based planning
    if let Some(plan) = hours_plan {
        if budget_plan.is_some() {
            println!();
        }
        println!("With {:.0} hours, you can complete:", plan.hours);
        println!();

        if plan.fits.is_empty() {
            println!("  No tasks fit within this time.");
        } else {
            for task in &plan.fits {
                let status = if task.is_ready { "ready" } else { "unblocked" };
                println!(
                    "  [x] {} ({:.0}h, ${:.0}) - {}",
                    task.id, task.hours, task.cost, status
                );
            }
            println!("  Remaining: {:.0}h", plan.remaining);
        }

        if !plan.exceeds.is_empty() {
            println!();
            println!("Cannot fit:");
            for task in &plan.exceeds {
                println!(
                    "  [ ] {} ({:.0}h, ${:.0}) - exceeds remaining hours",
                    task.id, task.hours, task.cost
                );
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    use workgraph::graph::{Estimate, Node, Task, WorkGraph};
    use workgraph::parser::save_graph;

    fn make_task(id: &str, title: &str) -> Task {
        Task {
            id: id.to_string(),
            title: title.to_string(),
            ..Task::default()
        }
    }

    #[test]
    fn test_budget_plan_output_creation() {
        let mut graph = WorkGraph::new();

        let mut t1 = make_task("t1", "Task 1");
        t1.estimate = Some(Estimate {
            hours: Some(4.0),
            cost: Some(400.0),
        });

        graph.add_node(Node::Task(t1));

        let result = tasks_within_budget(&graph, 500.0);
        let plan = BudgetPlanOutput {
            budget: 500.0,
            fits: result
                .fits
                .iter()
                .map(|t| TaskPlanItem {
                    id: t.id.to_string(),
                    title: t.title.to_string(),
                    cost: t.cost,
                    hours: t.hours,
                    is_ready: t.is_ready,
                })
                .collect(),
            exceeds: vec![],
            remaining: result.remaining,
        };

        assert_eq!(plan.budget, 500.0);
        assert_eq!(plan.fits.len(), 1);
        assert_eq!(plan.fits[0].id, "t1");
        assert_eq!(plan.remaining, 100.0);
    }

    #[test]
    fn test_hours_plan_output_creation() {
        let mut graph = WorkGraph::new();

        let mut t1 = make_task("t1", "Task 1");
        t1.estimate = Some(Estimate {
            hours: Some(4.0),
            cost: Some(400.0),
        });

        graph.add_node(Node::Task(t1));

        let result = tasks_within_hours(&graph, 10.0);
        let plan = HoursPlanOutput {
            hours: 10.0,
            fits: result
                .fits
                .iter()
                .map(|t| TaskPlanItem {
                    id: t.id.to_string(),
                    title: t.title.to_string(),
                    cost: t.cost,
                    hours: t.hours,
                    is_ready: t.is_ready,
                })
                .collect(),
            exceeds: vec![],
            remaining: result.remaining,
        };

        assert_eq!(plan.hours, 10.0);
        assert_eq!(plan.fits.len(), 1);
        assert_eq!(plan.fits[0].id, "t1");
        assert_eq!(plan.remaining, 6.0);
    }

    #[test]
    fn test_plan_output_serialization() {
        let summary = ProjectSummary {
            open: 2,
            done: 1,
            in_progress: 0,
            ready: 1,
            blocked: 1,
            total_cost: 1000.0,
            total_hours: 10.0,
        };

        let output = PlanOutput {
            summary,
            budget_plan: None,
            hours_plan: None,
        };

        let json = serde_json::to_string(&output).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed["summary"]["open"], 2);
        assert_eq!(parsed["summary"]["total_cost"], 1000.0);
    }

    fn setup_plan_graph(dir: &Path) {
        let path = dir.join("graph.jsonl");
        let mut graph = WorkGraph::new();

        let mut t1 = make_task("t1", "Task 1");
        t1.estimate = Some(Estimate {
            hours: Some(4.0),
            cost: Some(400.0),
        });

        let mut t2 = make_task("t2", "Task 2");
        t2.estimate = Some(Estimate {
            hours: Some(8.0),
            cost: Some(800.0),
        });

        graph.add_node(Node::Task(t1));
        graph.add_node(Node::Task(t2));
        save_graph(&graph, &path).unwrap();
    }

    #[test]
    fn test_run_no_constraints() {
        let temp_dir = TempDir::new().unwrap();
        setup_plan_graph(temp_dir.path());
        assert!(run(temp_dir.path(), None, None, false).is_ok());
    }

    #[test]
    fn test_run_with_budget() {
        let temp_dir = TempDir::new().unwrap();
        setup_plan_graph(temp_dir.path());
        assert!(run(temp_dir.path(), Some(500.0), None, false).is_ok());
    }

    #[test]
    fn test_run_with_hours() {
        let temp_dir = TempDir::new().unwrap();
        setup_plan_graph(temp_dir.path());
        assert!(run(temp_dir.path(), None, Some(6.0), false).is_ok());
    }

    #[test]
    fn test_run_json_output() {
        let temp_dir = TempDir::new().unwrap();
        setup_plan_graph(temp_dir.path());
        assert!(run(temp_dir.path(), Some(500.0), Some(6.0), true).is_ok());
    }

    #[test]
    fn test_run_empty_graph() {
        let temp_dir = TempDir::new().unwrap();
        let path = temp_dir.path().join("graph.jsonl");
        save_graph(&WorkGraph::new(), &path).unwrap();
        assert!(run(temp_dir.path(), None, None, false).is_ok());
    }

    #[test]
    fn test_run_no_init() {
        let temp_dir = TempDir::new().unwrap();
        assert!(run(temp_dir.path(), None, None, false).is_err());
    }

    #[test]
    fn test_run_budget_and_hours() {
        let temp_dir = TempDir::new().unwrap();
        setup_plan_graph(temp_dir.path());
        assert!(run(temp_dir.path(), Some(1500.0), Some(20.0), false).is_ok());
    }
}
