use clap::{Args, Subcommand};
use homeboy::triage::{self, TriageOptions, TriageOutput, TriageTarget};
use homeboy::Error;
use std::path::PathBuf;

use super::CmdResult;

#[derive(Args)]
pub struct TriageArgs {
    #[command(subcommand)]
    command: Option<TriageCommand>,

    /// Include issues in the report. Defaults to issues + PRs when neither is set.
    #[arg(long, global = true)]
    issues: bool,

    /// Include pull requests in the report. Defaults to issues + PRs when neither is set.
    #[arg(long, global = true)]
    prs: bool,

    /// Show work assigned to or authored by the authenticated GitHub user.
    #[arg(long, global = true)]
    mine: bool,

    /// Restrict to issues/PRs assigned to this GitHub user.
    #[arg(long, global = true, value_name = "USER")]
    assigned: Option<String>,

    /// Restrict to items carrying this label. Repeatable.
    #[arg(long, global = true, value_name = "LABEL")]
    label: Vec<String>,

    /// Fetch this issue number exactly. Repeatable.
    #[arg(long, global = true, value_name = "NUMBER")]
    issue: Vec<u64>,

    /// Read issue numbers from a newline-separated file.
    #[arg(long, global = true, value_name = "PATH")]
    issues_from_file: Option<PathBuf>,

    /// Restrict PRs to review-required items.
    #[arg(long, global = true)]
    needs_review: bool,

    /// Restrict PRs to failing-check items.
    #[arg(long, global = true)]
    failing_checks: bool,

    /// Include compact failing check names and URLs for failing PRs.
    #[arg(long, global = true)]
    drilldown: bool,

    /// Mark issues/PRs stale after this many days (`14` or `14d`).
    #[arg(long, global = true, value_name = "DAYS")]
    stale: Option<String>,

    /// Maximum items fetched per repo for each item type.
    #[arg(long, global = true, default_value_t = 30)]
    limit: usize,
}

#[derive(Subcommand, Debug)]
enum TriageCommand {
    /// Triage one registered component.
    ///
    /// When `--path <CHECKOUT>` is supplied, the registry is bypassed and the
    /// GitHub remote is resolved directly from the checkout's `origin`. Useful
    /// for unregistered checkouts (CI runners, ad-hoc clones, worktrees) or
    /// when a component's registry record is broken.
    Component {
        /// Component ID. Optional when `--path` is supplied.
        component_id: Option<String>,

        /// Workspace path to triage directly, bypassing the registry.
        #[arg(long, value_name = "CHECKOUT")]
        path: Option<String>,
    },
    /// Triage every component attached to a project.
    Project { project_id: String },
    /// Triage unique components used across a fleet.
    Fleet { fleet_id: String },
    /// Triage components declared in a local rig spec.
    Rig { rig_id: String },
    /// Triage every configured project, rig, and registered component once per repo.
    Workspace,
}

pub fn run(args: TriageArgs, _global: &super::GlobalArgs) -> CmdResult<TriageOutput> {
    let mut issue_numbers = args.issue;
    if let Some(path) = args.issues_from_file {
        issue_numbers.extend(triage::parse_issue_numbers_file(&path)?);
    }
    issue_numbers.sort_unstable();
    issue_numbers.dedup();

    let include_issues = args.issues || !args.prs || !issue_numbers.is_empty();
    let include_prs = args.prs || !args.issues;
    let options = TriageOptions {
        include_issues,
        include_prs,
        mine: args.mine,
        assigned: args.assigned,
        labels: args.label,
        needs_review: args.needs_review,
        failing_checks: args.failing_checks,
        drilldown: args.drilldown,
        issue_numbers,
        stale_days: match args.stale {
            Some(value) => Some(triage::parse_stale_days(&value)?),
            None => None,
        },
        limit: args.limit,
    };

    let target = match args.command.unwrap_or(TriageCommand::Workspace) {
        TriageCommand::Component { component_id, path } => {
            resolve_component_target(component_id, path)?
        }
        TriageCommand::Project { project_id } => TriageTarget::Project(project_id),
        TriageCommand::Fleet { fleet_id } => TriageTarget::Fleet(fleet_id),
        TriageCommand::Rig { rig_id } => TriageTarget::Rig(rig_id),
        TriageCommand::Workspace => TriageTarget::Workspace,
    };

    Ok((triage::run(target, options)?, 0))
}

fn resolve_component_target(
    component_id: Option<String>,
    path: Option<String>,
) -> Result<TriageTarget, Error> {
    match (component_id, path) {
        (None, None) => Err(Error::validation_missing_argument(vec![
            "component_id".into(),
            "path".into(),
        ])),
        (Some(component_id), None) => Ok(TriageTarget::Component(component_id)),
        (component_id, Some(path)) => {
            // When both are supplied, verify the registry record (if any) points at
            // the same checkout. If it does not, surface a clear error rather than
            // silently picking one side. If the component is not registered, we
            // accept the explicit id as the synthetic component_id.
            if let Some(ref id) = component_id {
                if let Ok(comp) = homeboy::component::load(id) {
                    let registered = canonicalize_for_compare(&comp.local_path);
                    let supplied = canonicalize_for_compare(&path);
                    if registered != supplied {
                        return Err(Error::validation_invalid_argument(
                            "path",
                            format!(
                                "Disagrees with registered component '{id}' (local_path={})",
                                comp.local_path
                            ),
                            Some(path),
                            None,
                        ));
                    }
                }
            }
            Ok(TriageTarget::Path { path, component_id })
        }
    }
}

fn canonicalize_for_compare(path: &str) -> String {
    std::path::Path::new(path)
        .canonicalize()
        .ok()
        .and_then(|p| p.to_str().map(|s| s.to_string()))
        .unwrap_or_else(|| path.to_string())
}

#[cfg(test)]
mod tests {
    use super::{resolve_component_target, TriageArgs, TriageCommand};
    use clap::Parser;
    use homeboy::triage::TriageTarget;

    #[derive(Parser)]
    struct TestCli {
        #[command(flatten)]
        args: TriageArgs,
    }

    #[test]
    fn bare_triage_defaults_to_workspace() {
        let cli = TestCli::parse_from(["triage"]);

        assert!(cli.args.command.is_none());
    }

    #[test]
    fn explicit_triage_subcommand_still_parses() {
        let cli = TestCli::parse_from(["triage", "workspace"]);

        assert!(matches!(cli.args.command, Some(TriageCommand::Workspace)));
    }

    #[test]
    fn component_subcommand_accepts_path_without_id() {
        let cli = TestCli::parse_from(["triage", "component", "--path", "/tmp/checkout"]);

        match cli.args.command {
            Some(TriageCommand::Component { component_id, path }) => {
                assert_eq!(component_id, None);
                assert_eq!(path.as_deref(), Some("/tmp/checkout"));
            }
            other => panic!("expected Component subcommand, got {other:?}"),
        }
    }

    #[test]
    fn component_subcommand_accepts_id_and_path() {
        let cli =
            TestCli::parse_from(["triage", "component", "homeboy", "--path", "/tmp/checkout"]);

        match cli.args.command {
            Some(TriageCommand::Component { component_id, path }) => {
                assert_eq!(component_id.as_deref(), Some("homeboy"));
                assert_eq!(path.as_deref(), Some("/tmp/checkout"));
            }
            other => panic!("expected Component subcommand, got {other:?}"),
        }
    }

    #[test]
    fn component_subcommand_requires_id_or_path() {
        let err = resolve_component_target(None, None).unwrap_err();
        assert_eq!(err.code.as_str(), "validation.missing_argument");
    }

    #[test]
    fn component_subcommand_routes_path_to_path_target() {
        let target = resolve_component_target(None, Some("/tmp/some-checkout".into())).unwrap();
        match target {
            TriageTarget::Path { path, component_id } => {
                assert_eq!(path, "/tmp/some-checkout");
                assert_eq!(component_id, None);
            }
            other => panic!("expected TriageTarget::Path, got {other:?}"),
        }
    }
}
