use std::collections::HashMap;
use std::path::Path;

use crate::component::Component;
use crate::context::RemoteProjectContext;
use crate::error::{Error, Result};
use crate::git;
use crate::project::Project;
use crate::version;

use super::execution::execute_component_deploy;
use super::planning::{
    calculate_component_status, calculate_release_state, load_project_components, plan_components,
};
use super::types::{
    ComponentDeployResult, ComponentStatus, DeployConfig, DeployOrchestrationResult, DeploySummary,
};
use super::version_overrides::fetch_remote_versions;

/// Main deploy orchestration entry point.
/// Handles component selection, building, and deployment.
pub(super) fn deploy_components(
    config: &DeployConfig,
    project: &Project,
    ctx: &RemoteProjectContext,
    base_path: &str,
) -> Result<DeployOrchestrationResult> {
    let loaded = load_project_components(project, &config.component_ids)?;
    if loaded.deployable.is_empty() {
        let message = if loaded.skipped.is_empty() {
            "No components configured for project".to_string()
        } else {
            format!(
                "No deployable components found — {} component(s) skipped (no build artifact or deploy strategy): {}",
                loaded.skipped.len(),
                loaded.skipped.join(", ")
            )
        };
        return Err(Error::validation_invalid_argument(
            "componentIds",
            message,
            None,
            Some(vec![
                "Ensure components have a buildArtifact, an extension with artifact_pattern, or deploy_strategy: \"git\"".to_string(),
                format!("Check with: homeboy component show <id>"),
            ]),
        ));
    }

    let components = plan_components(
        config,
        &loaded.deployable,
        &loaded.skipped,
        base_path,
        &ctx.client,
    )?;

    if components.is_empty() {
        return Ok(DeployOrchestrationResult {
            results: vec![],
            summary: DeploySummary {
                total: 0,
                succeeded: 0,
                failed: 0,
                skipped: 0,
            },
        });
    }

    // Gather versions
    let local_versions: HashMap<String, String> = components
        .iter()
        .filter_map(|c| version::get_component_version(c).map(|v| (c.id.clone(), v)))
        .collect();
    let remote_versions = if config.outdated || config.dry_run || config.check {
        fetch_remote_versions(&components, base_path, &ctx.client)
    } else {
        HashMap::new()
    };

    // Check and dry-run modes return early without building or deploying
    if config.check {
        return Ok(run_check_mode(
            &components,
            &local_versions,
            &remote_versions,
            base_path,
        ));
    }
    if config.dry_run {
        return Ok(run_dry_run_mode(
            &components,
            &local_versions,
            &remote_versions,
            base_path,
            config,
        ));
    }

    // Sync: pull latest changes before deploying (unless --no-pull or --skip-build)
    if !config.no_pull && !config.skip_build {
        sync_components(&components)?;
    }

    if !config.force {
        check_uncommitted_changes(&components)?;
    }

    // Smart artifact reuse: check if any component has a fresh build artifact
    // from a recent `homeboy build`. If so, skip the tag checkout and rebuild
    // for that component — deploy what was already built.
    let reuse_artifact_ids = if !config.head && !config.skip_build && !config.tagged {
        detect_reusable_artifacts(&components)
    } else {
        Vec::new()
    };

    // Warn about HEAD-vs-tag gap before the tag checkout.
    // Skip warning for components that will reuse their build artifact.
    if !config.head && !config.skip_build {
        let non_reuse: Vec<_> = components
            .iter()
            .filter(|c| !reuse_artifact_ids.contains(&c.id))
            .cloned()
            .collect();
        if !non_reuse.is_empty() {
            warn_unreleased_commits(&non_reuse, config);
        }
    }

    // Checkout latest tag for each component (unless --head or reusing artifact).
    // Components with reusable build artifacts are excluded from tag checkout.
    let tag_checkouts = if !config.head && !config.skip_build {
        let checkout_components: Vec<_> = components
            .iter()
            .filter(|c| !reuse_artifact_ids.contains(&c.id))
            .cloned()
            .collect();
        if checkout_components.is_empty() {
            Vec::new()
        } else {
            checkout_latest_tags(&checkout_components)?
        }
    } else {
        Vec::new()
    };

    // Verify expected version if --version was specified
    if let Some(ref expected) = config.expected_version {
        verify_expected_version(&components, expected)?;
    }

    // Execute deployments
    let mut results: Vec<ComponentDeployResult> = vec![];
    let mut succeeded: u32 = 0;
    let mut failed: u32 = 0;

    for component in &components {
        // Apply per-project overrides (e.g. different extract_command or remote_owner)
        let component = crate::project::apply_component_overrides(component, project);

        // For components reusing a build artifact, skip the build step
        let effective_config = if reuse_artifact_ids.contains(&component.id) {
            DeployConfig {
                skip_build: true,
                ..clone_config(config)
            }
        } else {
            clone_config(config)
        };

        let mut result = execute_component_deploy(
            &component,
            &effective_config,
            ctx,
            base_path,
            project,
            local_versions.get(&component.id).cloned(),
            remote_versions.get(&component.id).cloned(),
        );

        // Record which git ref was deployed
        if reuse_artifact_ids.contains(&component.id) {
            // Artifact was reused from a prior build — record the provenance ref
            if let Some(prov) = super::provenance::read(&component) {
                let ref_label = if prov.is_ahead_of_tag() {
                    format!(
                        "{} (HEAD, {} ahead of {})",
                        prov.git_ref,
                        prov.ahead_of_tag,
                        prov.tag.as_deref().unwrap_or("?")
                    )
                } else {
                    format!("{} (build artifact)", prov.git_ref)
                };
                result = result.with_deployed_ref(ref_label);
            }
        } else if let Some(checkout) = tag_checkouts
            .iter()
            .find(|c| c.component_id == component.id)
        {
            result = result.with_deployed_ref(checkout.tag.clone());
        } else if config.head {
            // Deploying from HEAD — record the current branch
            if let Some(branch) = crate::engine::command::run_in_optional(
                &component.local_path,
                "git",
                &["rev-parse", "--abbrev-ref", "HEAD"],
            ) {
                result = result.with_deployed_ref(format!("{} (HEAD)", branch));
            }
        }

        if result.status == "deployed" {
            succeeded += 1;
        } else {
            failed += 1;
        }
        results.push(result);
    }

    // Restore original branches after deployment
    if !tag_checkouts.is_empty() {
        restore_branches(&tag_checkouts);
    }

    Ok(DeployOrchestrationResult {
        results,
        summary: DeploySummary {
            total: succeeded + failed,
            succeeded,
            failed,
            skipped: 0,
        },
    })
}

/// Check mode: return component status without building or deploying.
fn run_check_mode(
    components: &[Component],
    local_versions: &HashMap<String, String>,
    remote_versions: &HashMap<String, String>,
    base_path: &str,
) -> DeployOrchestrationResult {
    let results: Vec<ComponentDeployResult> = components
        .iter()
        .map(|c| {
            let status = calculate_component_status(c, remote_versions);
            let release_state = calculate_release_state(c);
            let mut result = ComponentDeployResult::new(c, base_path)
                .with_status("checked")
                .with_versions(
                    local_versions.get(&c.id).cloned(),
                    remote_versions.get(&c.id).cloned(),
                )
                .with_component_status(status);
            if let Some(state) = release_state {
                result = result.with_release_state(state);
            }
            result
        })
        .collect();

    let total = results.len() as u32;
    DeployOrchestrationResult {
        results,
        summary: DeploySummary {
            total,
            succeeded: 0,
            failed: 0,
            skipped: 0,
        },
    }
}

/// Dry-run mode: return planned results without building or deploying.
fn run_dry_run_mode(
    components: &[Component],
    local_versions: &HashMap<String, String>,
    remote_versions: &HashMap<String, String>,
    base_path: &str,
    config: &DeployConfig,
) -> DeployOrchestrationResult {
    let results: Vec<ComponentDeployResult> = components
        .iter()
        .map(|c| {
            let status = if config.check {
                calculate_component_status(c, remote_versions)
            } else {
                ComponentStatus::Unknown
            };
            let mut result = ComponentDeployResult::new(c, base_path)
                .with_status("planned")
                .with_versions(
                    local_versions.get(&c.id).cloned(),
                    remote_versions.get(&c.id).cloned(),
                );
            if config.check {
                result = result.with_component_status(status);
            }
            result
        })
        .collect();

    let total = results.len() as u32;
    DeployOrchestrationResult {
        results,
        summary: DeploySummary {
            total,
            succeeded: 0,
            failed: 0,
            skipped: 0,
        },
    }
}

/// Verify no components have uncommitted changes before deployment.
fn check_uncommitted_changes(components: &[Component]) -> Result<()> {
    let dirty: Vec<&str> = components
        .iter()
        .filter(|c| !git::is_workdir_clean(Path::new(&c.local_path)))
        .map(|c| c.id.as_str())
        .collect();

    if !dirty.is_empty() {
        return Err(Error::validation_invalid_argument(
            "components",
            format!("Components have uncommitted changes: {}", dirty.join(", ")),
            None,
            Some(vec![
                "Commit your changes before deploying to ensure deployed code is tracked"
                    .to_string(),
                "Use --force to deploy anyway".to_string(),
            ]),
        ));
    }
    Ok(())
}

/// Fetch and pull latest changes for each component before deploying.
///
/// Prevents deploying stale code when the local clone is behind remote.
/// Runs `git fetch` + `git pull` for each component that has an upstream.
/// Aborts if pull fails (e.g., merge conflicts).
fn sync_components(components: &[Component]) -> Result<()> {
    for component in components {
        let path = &component.local_path;

        // Check if behind remote
        match git::fetch_and_get_behind_count(path) {
            Ok(Some(behind)) => {
                log_status!(
                    "deploy",
                    "'{}' is {} commit(s) behind remote — pulling...",
                    component.id,
                    behind
                );
                let pull_result = git::pull(Some(&component.id))?;
                if !pull_result.success {
                    return Err(Error::git_command_failed(format!(
                        "Failed to pull '{}': {}",
                        component.id,
                        pull_result.stderr.lines().next().unwrap_or("unknown error")
                    )));
                }
                log_status!("deploy", "'{}' is now up to date", component.id);
            }
            Ok(None) => {
                // Not behind or no upstream — nothing to do
            }
            Err(_) => {
                // git fetch failed — warn but don't block (might be offline)
                log_status!(
                    "deploy",
                    "Warning: could not check remote status for '{}' — deploying local state",
                    component.id
                );
            }
        }
    }
    Ok(())
}

/// Record of a tag checkout for later branch restoration.
struct TagCheckout {
    component_id: String,
    tag: String,
    original_ref: String,
    local_path: String,
}

/// Checkout the latest version tag for each component before building.
///
/// For each component, finds the latest semver tag, saves the current
/// branch/ref, and checks out the tag. Returns a list of checkouts
/// so branches can be restored after deployment.
///
/// Components without tags are skipped with a warning — they deploy
/// from HEAD as before (the pre-tag-checkout behavior).
fn checkout_latest_tags(components: &[Component]) -> Result<Vec<TagCheckout>> {
    let mut checkouts = Vec::new();

    for component in components {
        let path = &component.local_path;

        // Get the latest tag
        let tag = match git::get_latest_tag(path) {
            Ok(Some(t)) => t,
            Ok(None) => {
                log_status!(
                    "deploy",
                    "Warning: '{}' has no version tags — deploying from HEAD (use --head to suppress this warning)",
                    component.id
                );
                continue;
            }
            Err(_) => {
                log_status!(
                    "deploy",
                    "Warning: could not read tags for '{}' — deploying from HEAD",
                    component.id
                );
                continue;
            }
        };

        // Save the current branch name. Use symbolic-ref which returns the
        // actual branch name and fails cleanly on detached HEAD (unlike
        // --abbrev-ref which returns the literal "HEAD" string). If HEAD is
        // already detached, save the commit hash so we can at least restore
        // to the same commit afterward.
        let original_ref = crate::engine::command::run_in_optional(
            path,
            "git",
            &["symbolic-ref", "--short", "HEAD"],
        )
        .or_else(|| {
            // Detached HEAD — save the commit hash as fallback
            crate::engine::command::run_in_optional(path, "git", &["rev-parse", "HEAD"])
        })
        .unwrap_or_else(|| "main".to_string());

        // If already on this tag's commit, skip checkout
        let tag_commit = crate::engine::command::run_in_optional(path, "git", &["rev-parse", &tag]);
        let head_commit =
            crate::engine::command::run_in_optional(path, "git", &["rev-parse", "HEAD"]);
        if tag_commit.is_some() && tag_commit == head_commit {
            log_status!(
                "deploy",
                "'{}' is already at tag {} — no checkout needed",
                component.id,
                tag
            );
            checkouts.push(TagCheckout {
                component_id: component.id.clone(),
                tag: tag.clone(),
                original_ref,
                local_path: path.clone(),
            });
            continue;
        }

        // Checkout the tag
        log_status!(
            "deploy",
            "'{}' checking out tag {} for deploy...",
            component.id,
            tag
        );
        match crate::engine::command::run_in(path, "git", &["checkout", &tag], "git checkout tag") {
            Ok(_) => {
                checkouts.push(TagCheckout {
                    component_id: component.id.clone(),
                    tag: tag.clone(),
                    original_ref,
                    local_path: path.clone(),
                });
            }
            Err(e) => {
                return Err(Error::git_command_failed(format!(
                    "Failed to checkout tag {} for '{}': {}",
                    tag, component.id, e
                )));
            }
        }
    }

    Ok(checkouts)
}

/// Restore original branches after deployment.
///
/// Best-effort: logs warnings on failure but does not abort.
/// The deployment already completed — failing to restore a branch
/// is inconvenient but not destructive.
fn restore_branches(checkouts: &[TagCheckout]) {
    for checkout in checkouts {
        let restore = crate::engine::command::run_in(
            &checkout.local_path,
            "git",
            &["checkout", &checkout.original_ref],
            "git checkout restore",
        );
        match restore {
            Ok(_) => {
                log_status!(
                    "deploy",
                    "'{}' restored to {}",
                    checkout.component_id,
                    checkout.original_ref
                );
            }
            Err(e) => {
                log_status!(
                    "deploy",
                    "Warning: could not restore '{}' to {}: {}",
                    checkout.component_id,
                    checkout.original_ref,
                    e
                );
            }
        }
    }
}

/// Warn about unreleased commits ahead of the latest tag.
///
/// Checks each component for commits between the latest tag and HEAD.
/// When found, logs a warning with the commit count and subjects so
/// the user knows their recent work won't be in this deploy.
fn warn_unreleased_commits(components: &[Component], config: &DeployConfig) {
    let mut any_gap = false;

    for component in components {
        if let Some(gap) = super::provenance::detect_tag_gap(component) {
            super::provenance::warn_tag_gap(&component.id, &gap, "deploy");
            any_gap = true;
        }
    }

    if any_gap {
        if config.force {
            log_status!(
                "deploy",
                "Deploying from tagged releases. Use `deploy --head` to include unreleased commits, or `homeboy release` to tag them."
            );
        } else {
            log_status!(
                "deploy",
                "Deploy will use tagged releases (not HEAD). Use `deploy --head` to include unreleased commits, or `homeboy release` to tag them."
            );
        }
    }
}

/// Detect components with reusable build artifacts.
///
/// A build artifact is reusable when:
/// 1. A `.homeboy-build-meta.json` sidecar exists
/// 2. The recorded commit matches the current HEAD (the artifact is current)
///
/// When a reusable artifact is found, the component skips tag checkout
/// and rebuild — deploying whatever was already built by `homeboy build`.
fn detect_reusable_artifacts(components: &[Component]) -> Vec<String> {
    let mut reuse_ids = Vec::new();

    for component in components {
        if let Some(prov) = super::provenance::read(component) {
            if super::provenance::is_current(component, &prov) {
                if prov.is_ahead_of_tag() {
                    log_status!(
                        "deploy",
                        "ℹ️  '{}': reusing build artifact from {} ({} commit(s) ahead of {})",
                        component.id,
                        &prov.commit[..prov.commit.len().min(8)],
                        prov.ahead_of_tag,
                        prov.tag.as_deref().unwrap_or("?")
                    );
                } else {
                    log_status!(
                        "deploy",
                        "ℹ️  '{}': reusing build artifact from {} (at tag {})",
                        component.id,
                        &prov.commit[..prov.commit.len().min(8)],
                        prov.tag.as_deref().unwrap_or("?")
                    );
                }
                reuse_ids.push(component.id.clone());
            }
        }
    }

    reuse_ids
}

/// Create a value copy of DeployConfig for per-component overrides.
fn clone_config(config: &DeployConfig) -> DeployConfig {
    DeployConfig {
        component_ids: config.component_ids.clone(),
        all: config.all,
        outdated: config.outdated,
        dry_run: config.dry_run,
        check: config.check,
        force: config.force,
        skip_build: config.skip_build,
        keep_deps: config.keep_deps,
        expected_version: config.expected_version.clone(),
        no_pull: config.no_pull,
        head: config.head,
        tagged: config.tagged,
    }
}

/// Verify that component versions match the expected version.
///
/// When `--version` is used, ensures the local version of each component
/// matches the asserted version. This catches cases where the local copy
/// has a different version than what was just released.
fn verify_expected_version(components: &[Component], expected: &str) -> Result<()> {
    let mut mismatches = Vec::new();

    for component in components {
        if let Some(local_version) = version::get_component_version(component) {
            if local_version != expected {
                mismatches.push(format!(
                    "'{}': local version is {} (expected {})",
                    component.id, local_version, expected
                ));
            }
        }
    }

    if !mismatches.is_empty() {
        return Err(Error::validation_invalid_argument(
            "version",
            format!("Version mismatch: {}", mismatches.join("; ")),
            None,
            Some(vec![
                "Pull latest changes: git pull".to_string(),
                "Or remove --version to deploy the current local version".to_string(),
            ]),
        ));
    }
    Ok(())
}
