use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

use serde::Serialize;

use crate::core::component::{self, Component};
use crate::core::deploy;
use crate::core::extension::{
    extension_ready_status, is_extension_compatible, is_extension_linked, load_all_extensions,
};
use crate::core::project::{self, Project};
use crate::core::release::{changelog, version};
use crate::core::server::{self, Server};
use crate::core::{git, Result};
use crate::{is_zero, is_zero_u32};

use super::{build_component_info, ComponentGap, ContextOutput};

#[derive(Debug, Serialize)]
pub struct GapSummary {
    pub component_id: String,
    pub field: String,
    pub reason: String,
    pub command: String,
}

#[derive(Debug, Serialize)]
pub struct ContextReportStatus {
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub ready_to_deploy: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub needs_release: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub docs_only: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub has_uncommitted: Vec<String>,
    #[serde(skip_serializing_if = "is_zero")]
    pub config_gaps: usize,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub gap_details: Vec<GapSummary>,
}

#[derive(Debug, Serialize)]
pub struct ContextReportSummary {
    pub total_components: usize,
    pub by_extension: HashMap<String, usize>,
    pub by_status: HashMap<String, usize>,
}

#[derive(Debug, Serialize)]
pub struct ComponentSummary {
    pub id: String,
    pub path: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub extensions: Vec<String>,
    pub status: String,
    #[serde(skip_serializing_if = "is_zero_u32")]
    pub commits_since_version: u32,
    #[serde(skip_serializing_if = "is_zero_u32")]
    pub code_commits: u32,
    #[serde(skip_serializing_if = "is_zero_u32")]
    pub docs_only_commits: u32,
}

#[derive(Debug, Serialize)]
pub struct ContextReport {
    pub command: String,
    pub status: ContextReportStatus,
    pub summary: ContextReportSummary,
    pub context: ContextOutput,
    pub next_steps: Vec<String>,
    pub components: Vec<ComponentSummary>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub servers: Vec<Server>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub projects: Vec<ProjectListItem>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub extensions: Vec<ExtensionEntry>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<VersionSnapshot>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git: Option<GitSnapshot>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_release: Option<ReleaseSnapshot>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub changelog: Option<ChangelogSnapshot>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub agent_context_files: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct ProjectListItem {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub domain: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub sub_targets: Vec<String>,
}

impl From<Project> for ProjectListItem {
    fn from(p: Project) -> Self {
        Self {
            id: p.id.clone(),
            domain: p.domain,
            sub_targets: p
                .sub_targets
                .iter()
                .filter_map(|st| project::slugify_id(&st.name).ok())
                .collect(),
        }
    }
}

#[derive(Debug, Serialize)]
pub struct ExtensionEntry {
    pub id: String,
    pub name: String,
    pub version: String,
    pub description: String,
    pub runtime: String,
    pub compatible: bool,
    pub ready: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ready_reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ready_detail: Option<String>,
    pub linked: bool,
}

#[derive(Debug, Serialize)]
pub struct VersionSnapshot {
    pub component_id: String,
    pub version: String,
    pub targets: Vec<version::VersionTargetInfo>,
}

#[derive(Debug, Serialize)]
pub struct GitSnapshot {
    pub branch: String,
    pub clean: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ahead: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub behind: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub commits_since_version: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub baseline_ref: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub baseline_warning: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ReleaseSnapshot {
    pub tag: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub date: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ChangelogSnapshot {
    pub path: String,
    pub label: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub items: Option<Vec<String>>,
}

pub type ComponentReleaseState = crate::core::deploy::ReleaseState;

#[derive(Debug, Clone, Serialize)]
pub struct ComponentWithState {
    #[serde(flatten)]
    pub component: Component,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub release_state: Option<ComponentReleaseState>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub gaps: Vec<ComponentGap>,
}

pub fn build_report(show_all_flag: bool, command: &str) -> Result<ContextReport> {
    build_report_at(show_all_flag, command, None, None)
}

pub fn build_report_for_component(
    show_all_flag: bool,
    command: &str,
    component: Component,
    path: Option<&str>,
) -> Result<ContextReport> {
    build_report_at(show_all_flag, command, path, Some(component))
}

fn build_report_at(
    show_all_flag: bool,
    command: &str,
    path: Option<&str>,
    focused_component: Option<Component>,
) -> Result<ContextReport> {
    let (context_output, _) = super::run(path)?;

    let relevant_ids: HashSet<String> = context_output
        .matched_components
        .iter()
        .chain(context_output.contained_components.iter())
        .cloned()
        .collect();

    let all_components = component::inventory().unwrap_or_default();
    let all_projects = project::list().unwrap_or_default();
    let all_servers = server::list().unwrap_or_default();
    let all_extensions = load_all_extensions().unwrap_or_default();

    let show_all = show_all_flag || relevant_ids.is_empty();
    let filtered_components = if let Some(component) = focused_component {
        if show_all_flag {
            let mut components = all_components;
            if !components.iter().any(|c| c.id == component.id) {
                components.push(component);
            }
            components
        } else {
            vec![component]
        }
    } else {
        collect_focused_components(show_all, &relevant_ids, all_components, &all_projects)
    };

    let cwd = path
        .map(PathBuf::from)
        .or_else(|| std::env::current_dir().ok());
    let components_with_state: Vec<ComponentWithState> = filtered_components
        .into_iter()
        .map(|component| {
            let release_state = deploy::calculate_release_state(&component);
            let gaps = if let Some(ref cwd_path) = cwd {
                if component::resolution::component_is_contained_in_path(&component, cwd_path) {
                    build_component_info(&component).gaps
                } else {
                    Vec::new()
                }
            } else {
                Vec::new()
            };
            ComponentWithState {
                component,
                release_state,
                gaps,
            }
        })
        .collect();

    let release_buckets = deploy::bucket_release_states(
        components_with_state
            .iter()
            .map(|comp| (comp.component.id.as_str(), comp.release_state.as_ref())),
    );
    let status = compute_status(&components_with_state, &release_buckets);
    let summary = compute_summary(&components_with_state);

    let linked_extension_ids: HashSet<String> = components_with_state
        .iter()
        .filter_map(|c| c.component.extensions.as_ref())
        .flat_map(|m| m.keys().cloned())
        .collect();

    let extensions: Vec<ExtensionEntry> = all_extensions
        .iter()
        .filter(|m| show_all || linked_extension_ids.contains(&m.id) || m.executable.is_none())
        .map(|m| {
            let ready_status = extension_ready_status(m);
            ExtensionEntry {
                id: m.id.clone(),
                name: m.name.clone(),
                version: m.version.clone(),
                description: m
                    .description
                    .as_ref()
                    .and_then(|d| d.lines().next())
                    .unwrap_or("")
                    .to_string(),
                runtime: if m.executable.is_some() {
                    "executable"
                } else {
                    "platform"
                }
                .to_string(),
                compatible: is_extension_compatible(m, None),
                ready: ready_status.ready,
                ready_reason: ready_status.reason,
                ready_detail: ready_status.detail,
                linked: is_extension_linked(&m.id),
            }
        })
        .collect();

    let filtered_projects: Vec<Project> = if show_all {
        all_projects
    } else {
        all_projects
            .into_iter()
            .filter(|p| p.components.iter().any(|c| relevant_ids.contains(&c.id)))
            .collect()
    };

    let relevant_server_ids: HashSet<String> = filtered_projects
        .iter()
        .filter_map(|p| p.server_id.clone())
        .collect();

    let projects: Vec<ProjectListItem> = filtered_projects
        .into_iter()
        .map(ProjectListItem::from)
        .collect();

    let servers: Vec<Server> = if show_all {
        all_servers
    } else {
        all_servers
            .into_iter()
            .filter(|s| relevant_server_ids.contains(&s.id))
            .collect()
    };

    let next_steps = build_actionable_next_steps(
        &status,
        &context_output,
        &components_with_state,
        &projects,
        &linked_extension_ids,
        &all_extensions,
    );

    let version_snapshot = if context_output.managed {
        resolve_version_snapshot(&components_with_state)
    } else {
        None
    };
    let git_snapshot = resolve_git_snapshot(
        context_output.git_root.as_ref(),
        version_snapshot.as_ref().map(|v| v.version.as_str()),
    );
    let (last_release, changelog_snapshot) = resolve_changelog_snapshots(&components_with_state);

    let mut warnings = validate_version_targets(&components_with_state);
    if let Some(alignment_warning) =
        validate_version_baseline_alignment(&version_snapshot, &git_snapshot)
    {
        warnings.push(alignment_warning);
    }

    let agent_context_files = resolve_agent_context_files(context_output.git_root.as_ref());
    let components = build_component_summaries(&components_with_state, cwd.as_ref());

    Ok(ContextReport {
        command: command.to_string(),
        status,
        summary,
        context: context_output,
        next_steps,
        components,
        servers,
        projects,
        extensions,
        version: version_snapshot,
        git: git_snapshot,
        last_release,
        changelog: changelog_snapshot,
        agent_context_files,
        warnings,
    })
}

fn collect_focused_components(
    show_all: bool,
    relevant_ids: &HashSet<String>,
    all_components: Vec<Component>,
    all_projects: &[Project],
) -> Vec<Component> {
    if show_all {
        return all_components;
    }

    let mut by_id: HashMap<String, Component> = all_components
        .into_iter()
        .filter(|c| relevant_ids.contains(&c.id))
        .map(|component| (component.id.clone(), component))
        .collect();

    for project in all_projects {
        for attachment in &project.components {
            if !relevant_ids.contains(&attachment.id) || by_id.contains_key(&attachment.id) {
                continue;
            }

            if let Some(mut component) =
                component::discover_from_portable(Path::new(&attachment.local_path))
            {
                component.id = attachment.id.clone();
                by_id.insert(component.id.clone(), component);
            }
        }
    }

    by_id.into_values().collect()
}

fn compute_status(
    components: &[ComponentWithState],
    release_buckets: &crate::core::deploy::ReleaseStateBuckets,
) -> ContextReportStatus {
    let mut config_gaps = 0;
    let mut gap_details = Vec::new();

    for comp in components {
        let id = &comp.component.id;

        for gap in &comp.gaps {
            config_gaps += 1;
            gap_details.push(GapSummary {
                component_id: id.clone(),
                field: gap.field.clone(),
                reason: gap.reason.clone(),
                command: gap.command.clone(),
            });
        }
    }

    ContextReportStatus {
        ready_to_deploy: release_buckets.ready_to_deploy.clone(),
        needs_release: release_buckets.needs_release.clone(),
        docs_only: release_buckets.docs_only.clone(),
        has_uncommitted: release_buckets.has_uncommitted.clone(),
        config_gaps,
        gap_details,
    }
}

fn compute_summary(components: &[ComponentWithState]) -> ContextReportSummary {
    let mut by_extension: HashMap<String, usize> = HashMap::new();
    let mut by_status: HashMap<String, usize> = HashMap::new();

    for comp in components {
        if let Some(ref extensions) = comp.component.extensions {
            for extension_id in extensions.keys() {
                *by_extension.entry(extension_id.clone()).or_insert(0) += 1;
            }
        }

        let status = deploy::classify_release_state(comp.release_state.as_ref())
            .as_str()
            .to_string();
        *by_status.entry(status).or_insert(0) += 1;
    }

    ContextReportSummary {
        total_components: components.len(),
        by_extension,
        by_status,
    }
}

fn shorten_path(path: &str, cwd: Option<&PathBuf>) -> String {
    let path_buf = PathBuf::from(path);
    if let Some(cwd_path) = cwd {
        if let Ok(relative) = path_buf.strip_prefix(cwd_path) {
            let rel_str = relative.to_string_lossy().to_string();
            if !rel_str.is_empty() {
                return rel_str;
            }
            return ".".to_string();
        }
    }

    if let Ok(home_str) = std::env::var("HOME") {
        let home = PathBuf::from(&home_str);
        if let Ok(relative) = path_buf.strip_prefix(&home) {
            return format!("~/{}", relative.to_string_lossy());
        }
    }
    path.to_string()
}

fn build_component_summaries(
    components: &[ComponentWithState],
    cwd: Option<&PathBuf>,
) -> Vec<ComponentSummary> {
    components
        .iter()
        .map(|comp| {
            let status = deploy::classify_release_state(comp.release_state.as_ref())
                .as_str()
                .to_string();
            let (commits, code, docs) = comp
                .release_state
                .as_ref()
                .map(|s| (s.commits_since_version, s.code_commits, s.docs_only_commits))
                .unwrap_or((0, 0, 0));

            let mut extensions = comp
                .component
                .extensions
                .as_ref()
                .map(|m| m.keys().cloned().collect::<Vec<_>>())
                .unwrap_or_default();
            extensions.sort();

            ComponentSummary {
                id: comp.component.id.clone(),
                path: shorten_path(&comp.component.local_path, cwd),
                extensions,
                status,
                commits_since_version: commits,
                code_commits: code,
                docs_only_commits: docs,
            }
        })
        .collect()
}

fn build_actionable_next_steps(
    status: &ContextReportStatus,
    context_output: &ContextOutput,
    components: &[ComponentWithState],
    projects: &[ProjectListItem],
    linked_extension_ids: &HashSet<String>,
    all_extensions: &[crate::core::extension::ExtensionManifest],
) -> Vec<String> {
    let mut next_steps = Vec::new();

    if !status.has_uncommitted.is_empty() {
        let count = status.has_uncommitted.len();
        if count == 1 {
            next_steps.push(format!(
                "1 component has uncommitted changes: `{}`",
                status.has_uncommitted[0]
            ));
        } else {
            next_steps.push(format!(
                "{} components have uncommitted changes. Review with `homeboy release changes <id>`.",
                count
            ));
        }
    }

    if !status.needs_release.is_empty() {
        let count = status.needs_release.len();
        if count == 1 {
            next_steps.push(format!(
                "1 component has unreleased commits: `{}`. Release with `homeboy release {}`.",
                status.needs_release[0], status.needs_release[0]
            ));
        } else {
            next_steps.push(format!(
                "{} components have unreleased commits. Release with `homeboy release <id>`.",
                count
            ));
        }
    }

    if !status.docs_only.is_empty() {
        let count = status.docs_only.len();
        if count == 1 {
            next_steps.push(format!(
                "1 component has docs-only changes: `{}`. No version bump needed.",
                status.docs_only[0]
            ));
        } else {
            next_steps.push(format!(
                "{} components have docs-only changes. No version bump needed.",
                count
            ));
        }
    }

    if !status.ready_to_deploy.is_empty() && status.has_uncommitted.is_empty() {
        let count = status.ready_to_deploy.len();
        if count == 1 {
            next_steps.push(format!(
                "1 component has a clean release tag: `{}`. This is git state only — run `homeboy status <project>` to confirm the deploy target is actually behind before `homeboy deploy {}`.",
                status.ready_to_deploy[0], status.ready_to_deploy[0]
            ));
        } else {
            next_steps.push(format!(
                "{} components have a clean release tag (git state only — does not mean the deploy target is behind). Run `homeboy status <project>` for a target-accurate diff before deploying.",
                count
            ));
        }
    }

    if status.config_gaps > 0 {
        next_steps.push(format!(
            "{} config gaps detected. Run `homeboy component show <id>` for details.",
            status.config_gaps
        ));
    }

    if context_output.managed && !components.is_empty() {
        let comp_id = &components[0].component.id;
        next_steps.push(format!(
            "You're in {}. Common: `homeboy build`, `homeboy deploy`, `homeboy release`.",
            comp_id
        ));
    }

    if Path::new("CLAUDE.md").exists() || Path::new("AGENTS.md").exists() {
        next_steps.push(
            "Read CLAUDE.md for repo-specific guidance. Run `homeboy self docs commands/commands-index` for all commands.".to_string(),
        );
    }

    let cli_extensions: Vec<_> = all_extensions
        .iter()
        .filter(|m| linked_extension_ids.contains(&m.id))
        .filter_map(|m| {
            m.cli
                .as_ref()
                .map(|c| (c.tool.clone(), c.display_name.clone()))
        })
        .collect();

    if !cli_extensions.is_empty() && !projects.is_empty() {
        let project_id = &projects[0].id;
        for (tool, display_name) in &cli_extensions {
            next_steps.push(format!(
                "Run remote {} commands: `homeboy {} {} <command>`.",
                display_name, tool, project_id
            ));
        }
    }

    if let Some(suggestion) = context_output.suggestion.as_ref() {
        next_steps.push(format!("Suggestion: {}", suggestion));
    }

    let mut outdated_extensions = Vec::new();
    for extension in all_extensions {
        if let Some(update) = crate::core::extension::check_update_available(&extension.id) {
            outdated_extensions.push(update);
        }
    }
    if !outdated_extensions.is_empty() {
        for update in &outdated_extensions {
            next_steps.push(format!(
                "Extension '{}' is outdated (v{}, {} commit{} behind). Run: `homeboy extension update {}`",
                update.extension_id,
                update.installed_version,
                update.behind_count,
                if update.behind_count == 1 { "" } else { "s" },
                update.extension_id,
            ));
        }
    }

    if components.is_empty() && !context_output.managed {
        next_steps.push(
            "Create a project: `homeboy project create <name> <domain> --server <id> --extension <id>`.".to_string(),
        );
        next_steps.push(
            "Create a component: `homeboy component create <name> --local-path . --remote-path <path> --project <id>`.".to_string(),
        );
    }

    next_steps
}

fn resolve_version_snapshot(components: &[ComponentWithState]) -> Option<VersionSnapshot> {
    let wrapper = components.first()?;
    let component = &wrapper.component;
    let snapshot = version::read_component_snapshot(component).ok()?;
    Some(VersionSnapshot {
        component_id: snapshot.component_id,
        version: snapshot.version,
        targets: snapshot.targets,
    })
}

fn resolve_git_snapshot(
    git_root: Option<&String>,
    current_version: Option<&str>,
) -> Option<GitSnapshot> {
    let root = git_root?;
    let snapshot = git::build_repo_baseline_snapshot(root, current_version).ok()?;
    Some(GitSnapshot {
        branch: snapshot.branch,
        clean: snapshot.clean,
        ahead: snapshot.ahead,
        behind: snapshot.behind,
        commits_since_version: snapshot.commits_since_version,
        baseline_ref: snapshot.baseline_ref,
        baseline_warning: snapshot.baseline_warning,
    })
}

fn resolve_changelog_snapshots(
    components: &[ComponentWithState],
) -> (Option<ReleaseSnapshot>, Option<ChangelogSnapshot>) {
    let wrapper = match components.first() {
        Some(c) => c,
        None => return (None, None),
    };
    let component = &wrapper.component;

    let (last_release, changelog_snapshot) = match changelog::read_component_snapshots(component) {
        Ok((last_release, changelog_snapshot)) => (last_release, changelog_snapshot),
        Err(_) => return (None, None),
    };

    (
        last_release.map(|snapshot| ReleaseSnapshot {
            tag: snapshot.tag,
            date: snapshot.date,
            summary: snapshot.summary,
        }),
        changelog_snapshot.map(|snapshot| ChangelogSnapshot {
            path: snapshot.path,
            label: snapshot.label,
            items: if snapshot.items.is_empty() {
                None
            } else {
                Some(snapshot.items)
            },
        }),
    )
}

fn resolve_agent_context_files(git_root: Option<&String>) -> Vec<String> {
    let root = match git_root {
        Some(r) => r,
        None => return Vec::new(),
    };

    let path = PathBuf::from(root);
    git::list_tracked_markdown_files(&path).unwrap_or_default()
}

fn validate_version_targets(components: &[ComponentWithState]) -> Vec<String> {
    components
        .iter()
        .flat_map(|wrapper| version::build_init_warnings(&wrapper.component))
        .collect()
}

fn validate_version_baseline_alignment(
    version: &Option<VersionSnapshot>,
    git: &Option<GitSnapshot>,
) -> Option<String> {
    let version_snapshot = version
        .as_ref()
        .map(|snapshot| version::ComponentVersionSnapshot {
            component_id: snapshot.component_id.clone(),
            version: snapshot.version.clone(),
            targets: snapshot.targets.clone(),
        });

    version::validate_baseline_alignment(
        version_snapshot.as_ref(),
        git.as_ref()
            .and_then(|snapshot| snapshot.baseline_ref.as_deref()),
    )
}
