//! Vendor command implementation
//!
//! Copies all dependencies to a local vendor/ directory for offline builds.
//! This ensures reproducible builds without network access.

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};
use clap::Args;
use serde::{Deserialize, Serialize};

use crate::config::CcgoConfig;
use crate::lockfile::{Lockfile, LockedPackage, LOCKFILE_NAME};

/// Default vendor directory name
pub const VENDOR_DIR: &str = "vendor";

/// Vendor metadata filename
pub const VENDOR_TOML: &str = ".vendor.toml";

/// Vendor all dependencies to project local directory
#[derive(Args, Debug)]
pub struct VendorCommand {
    /// Don't delete unused dependencies from vendor/
    #[arg(long)]
    pub no_delete: bool,

    /// Sync vendor/ with lockfile, re-vendoring changed dependencies
    #[arg(long)]
    pub sync: bool,

    /// Only verify vendor/ integrity without modifying
    #[arg(long)]
    pub verify: bool,

    /// Custom vendor directory path
    #[arg(long, default_value = VENDOR_DIR)]
    pub path: String,

    /// Remove .git directories from vendored dependencies
    #[arg(long, default_value = "true")]
    pub strip_git: bool,
}

/// Vendor metadata file structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VendorConfig {
    /// Vendor config version
    pub version: u32,

    /// Metadata
    #[serde(default)]
    pub metadata: VendorMetadata,

    /// Vendored packages
    #[serde(default, rename = "package")]
    pub packages: Vec<VendoredPackage>,
}

/// Vendor metadata
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VendorMetadata {
    /// When vendor was last run
    #[serde(skip_serializing_if = "Option::is_none")]
    pub generated_at: Option<String>,

    /// CCGO version
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ccgo_version: Option<String>,

    /// Source lockfile hash for change detection
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lockfile_hash: Option<String>,
}

/// A vendored package entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VendoredPackage {
    /// Package name
    pub name: String,

    /// Package version
    pub version: String,

    /// Original source
    pub source: String,

    /// When this package was vendored
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vendored_at: Option<String>,

    /// Checksum of vendored content
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checksum: Option<String>,
}

impl Default for VendorConfig {
    fn default() -> Self {
        Self::new()
    }
}

impl VendorConfig {
    /// Create new vendor config
    pub fn new() -> Self {
        Self {
            version: 1,
            metadata: VendorMetadata {
                generated_at: Some(chrono::Local::now().to_rfc3339()),
                ccgo_version: Some(env!("CARGO_PKG_VERSION").to_string()),
                lockfile_hash: None,
            },
            packages: Vec::new(),
        }
    }

    /// Load vendor config from path
    pub fn load(vendor_dir: &Path) -> Result<Option<Self>> {
        let config_path = vendor_dir.join(VENDOR_TOML);
        if !config_path.exists() {
            return Ok(None);
        }

        let content = fs::read_to_string(&config_path)
            .with_context(|| format!("Failed to read {}", config_path.display()))?;

        let config: Self = toml::from_str(&content)
            .with_context(|| format!("Failed to parse {}", config_path.display()))?;

        Ok(Some(config))
    }

    /// Save vendor config
    pub fn save(&self, vendor_dir: &Path) -> Result<()> {
        let config_path = vendor_dir.join(VENDOR_TOML);

        let mut content = String::new();
        content.push_str("# Auto-generated by ccgo vendor\n");
        content.push_str("# Do not edit manually\n\n");
        content.push_str(&format!("version = {}\n\n", self.version));

        content.push_str("[metadata]\n");
        if let Some(ref generated_at) = self.metadata.generated_at {
            content.push_str(&format!("generated_at = \"{}\"\n", generated_at));
        }
        if let Some(ref ccgo_version) = self.metadata.ccgo_version {
            content.push_str(&format!("ccgo_version = \"{}\"\n", ccgo_version));
        }
        if let Some(ref lockfile_hash) = self.metadata.lockfile_hash {
            content.push_str(&format!("lockfile_hash = \"{}\"\n", lockfile_hash));
        }
        content.push('\n');

        for pkg in &self.packages {
            content.push_str("[[package]]\n");
            content.push_str(&format!("name = \"{}\"\n", pkg.name));
            content.push_str(&format!("version = \"{}\"\n", pkg.version));
            content.push_str(&format!("source = \"{}\"\n", pkg.source));
            if let Some(ref vendored_at) = pkg.vendored_at {
                content.push_str(&format!("vendored_at = \"{}\"\n", vendored_at));
            }
            if let Some(ref checksum) = pkg.checksum {
                content.push_str(&format!("checksum = \"{}\"\n", checksum));
            }
            content.push('\n');
        }

        fs::write(&config_path, content)
            .with_context(|| format!("Failed to write {}", config_path.display()))?;

        Ok(())
    }

    /// Get a vendored package by name
    pub fn get_package(&self, name: &str) -> Option<&VendoredPackage> {
        self.packages.iter().find(|p| p.name == name)
    }

    /// Add or update a package
    pub fn upsert_package(&mut self, pkg: VendoredPackage) {
        if let Some(existing) = self.packages.iter_mut().find(|p| p.name == pkg.name) {
            *existing = pkg;
        } else {
            self.packages.push(pkg);
        }
        self.packages.sort_by(|a, b| a.name.cmp(&b.name));
    }
}

impl VendorCommand {
    /// Execute the vendor command
    pub fn execute(self, verbose: bool) -> Result<()> {
        println!("{}", "=".repeat(80));
        println!("CCGO Vendor - Vendor Dependencies for Offline Builds");
        println!("{}", "=".repeat(80));

        let project_dir = std::env::current_dir().context("Failed to get current directory")?;
        let vendor_dir = project_dir.join(&self.path);

        println!("\nProject directory: {}", project_dir.display());
        println!("Vendor directory: {}", vendor_dir.display());

        // Load CCGO.toml
        let config = CcgoConfig::load().context("Failed to load CCGO.toml")?;

        // Load lockfile (required for vendor)
        let lockfile = Lockfile::load(&project_dir)?
            .ok_or_else(|| anyhow::anyhow!(
                "No {} found. Run 'ccgo install' first to generate a lockfile.",
                LOCKFILE_NAME
            ))?;

        let dependencies = &config.dependencies;
        if dependencies.is_empty() {
            println!("\n   ℹ️  No dependencies to vendor");
            return Ok(());
        }

        // Load existing vendor config
        let existing_vendor_config = VendorConfig::load(&vendor_dir)?;

        // Verify mode
        if self.verify {
            return self.verify_vendor(&vendor_dir, &lockfile, existing_vendor_config.as_ref());
        }

        // Create vendor directory
        fs::create_dir_all(&vendor_dir)
            .with_context(|| format!("Failed to create vendor directory: {}", vendor_dir.display()))?;

        println!("\n📦 Vendoring {} dependencies...", lockfile.packages.len());

        let ccgo_home = Self::get_ccgo_home();
        let mut vendor_config = VendorConfig::new();
        let mut vendored_count = 0;
        let mut skipped_count = 0;
        let mut failed_count = 0;

        // Track which packages we're vendoring (for cleanup)
        let mut vendored_names: Vec<String> = Vec::new();

        for locked_pkg in &lockfile.packages {
            vendored_names.push(locked_pkg.name.clone());

            // Check if already vendored and up-to-date
            if !self.sync {
                if let Some(ref existing) = existing_vendor_config {
                    if let Some(existing_pkg) = existing.get_package(&locked_pkg.name) {
                        if existing_pkg.source == locked_pkg.source {
                            if verbose {
                                println!("   ⏭️  {} already vendored", locked_pkg.name);
                            }
                            vendor_config.upsert_package(existing_pkg.clone());
                            skipped_count += 1;
                            continue;
                        }
                    }
                }
            }

            match self.vendor_package(locked_pkg, &project_dir, &vendor_dir, &ccgo_home, verbose) {
                Ok(vendored_pkg) => {
                    println!("   ✓ Vendored {}", locked_pkg.name);
                    vendor_config.upsert_package(vendored_pkg);
                    vendored_count += 1;
                }
                Err(e) => {
                    eprintln!("   ✗ Failed to vendor {}: {}", locked_pkg.name, e);
                    failed_count += 1;
                }
            }
        }

        // Clean up unused dependencies
        if !self.no_delete {
            self.cleanup_unused(&vendor_dir, &vendored_names, verbose)?;
        }

        // Save vendor config
        vendor_config.metadata.generated_at = Some(chrono::Local::now().to_rfc3339());
        vendor_config.save(&vendor_dir)?;

        // Summary
        println!("\n{}", "=".repeat(80));
        println!("Vendor Summary");
        println!("{}", "=".repeat(80));
        println!("\n✓ Vendored: {}", vendored_count);
        if skipped_count > 0 {
            println!("⏭️  Skipped (already vendored): {}", skipped_count);
        }
        if failed_count > 0 {
            println!("✗ Failed: {}", failed_count);
        }
        println!("\n📁 Vendor directory: {}", vendor_dir.display());

        // Usage hint
        println!("\n💡 To use vendored dependencies:");
        println!("   - Dependencies are now in {}/", self.path);
        println!("   - Commit vendor/ to version control for offline builds");

        if failed_count > 0 {
            bail!("{} package(s) failed to vendor", failed_count);
        }

        Ok(())
    }

    /// Vendor a single package
    fn vendor_package(
        &self,
        locked_pkg: &LockedPackage,
        project_dir: &Path,
        vendor_dir: &Path,
        ccgo_home: &Path,
        verbose: bool,
    ) -> Result<VendoredPackage> {
        let target_dir = vendor_dir.join(&locked_pkg.name);

        // Remove existing if present
        if target_dir.exists() {
            fs::remove_dir_all(&target_dir)
                .with_context(|| format!("Failed to remove existing vendor dir: {}", target_dir.display()))?;
        }

        // Determine source and copy
        let (source_type, source_path) = locked_pkg.parse_source();

        match source_type {
            crate::lockfile::SourceType::Git => {
                self.vendor_from_git(locked_pkg, &target_dir, ccgo_home, verbose)?;
            }
            crate::lockfile::SourceType::Path => {
                self.vendor_from_path(&source_path, &target_dir, project_dir, verbose)?;
            }
            _ => {
                bail!("Unsupported source type for vendoring: {:?}", source_type);
            }
        }

        // Strip .git directory if requested
        if self.strip_git {
            let git_dir = target_dir.join(".git");
            if git_dir.exists() {
                if verbose {
                    println!("      Removing .git directory");
                }
                fs::remove_dir_all(&git_dir).ok();
            }
        }

        Ok(VendoredPackage {
            name: locked_pkg.name.clone(),
            version: locked_pkg.version.clone(),
            source: locked_pkg.source.clone(),
            vendored_at: Some(chrono::Local::now().to_rfc3339()),
            checksum: None, // TODO: implement checksum
        })
    }

    /// Vendor from git cache
    fn vendor_from_git(
        &self,
        locked_pkg: &LockedPackage,
        target_dir: &Path,
        ccgo_home: &Path,
        verbose: bool,
    ) -> Result<()> {
        // Parse git URL from source
        let (_, git_url) = locked_pkg.parse_source();

        // Find in registry cache
        let hash_input = format!("{}:{}", locked_pkg.name, git_url);
        let hash = format!("{:x}", md5::compute(hash_input.as_bytes()));
        let registry_name = format!("{}-{}", locked_pkg.name, &hash[..16]);
        let registry_path = ccgo_home.join("registry").join(&registry_name);

        if !registry_path.exists() {
            // Need to clone first
            if verbose {
                println!("      Cloning {} (not in cache)", git_url);
            }

            let registry_dir = ccgo_home.join("registry");
            fs::create_dir_all(&registry_dir)?;

            let mut cmd = std::process::Command::new("git");
            cmd.args(["clone", &git_url, registry_path.to_string_lossy().as_ref()]);

            let output = cmd.output().context("Failed to execute git clone")?;
            if !output.status.success() {
                bail!("Git clone failed: {}", String::from_utf8_lossy(&output.stderr));
            }

            // Checkout specific revision if available
            if let Some(ref git_info) = locked_pkg.git {
                let checkout = std::process::Command::new("git")
                    .args(["checkout", &git_info.revision])
                    .current_dir(&registry_path)
                    .output()
                    .context("Failed to checkout revision")?;
                if !checkout.status.success() {
                    bail!("Git checkout failed: {}", String::from_utf8_lossy(&checkout.stderr));
                }
            }
        }

        // Copy from cache to vendor
        if verbose {
            println!("      Copying from cache: {}", registry_path.display());
        }
        Self::copy_dir_recursive(&registry_path, target_dir)?;

        Ok(())
    }

    /// Vendor from local path
    fn vendor_from_path(
        &self,
        path: &str,
        target_dir: &Path,
        project_dir: &Path,
        verbose: bool,
    ) -> Result<()> {
        let source_path = if Path::new(path).is_absolute() {
            PathBuf::from(path)
        } else {
            project_dir.join(path)
        };

        if !source_path.exists() {
            bail!("Source path does not exist: {}", source_path.display());
        }

        if verbose {
            println!("      Copying from: {}", source_path.display());
        }

        Self::copy_dir_recursive(&source_path, target_dir)?;

        Ok(())
    }

    /// Recursively copy directory
    fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
        fs::create_dir_all(dst)?;

        for entry in fs::read_dir(src)? {
            let entry = entry?;
            let src_path = entry.path();
            let dst_path = dst.join(entry.file_name());

            if src_path.is_dir() {
                Self::copy_dir_recursive(&src_path, &dst_path)?;
            } else {
                fs::copy(&src_path, &dst_path)?;
            }
        }

        Ok(())
    }

    /// Clean up unused vendored dependencies
    fn cleanup_unused(&self, vendor_dir: &Path, keep: &[String], verbose: bool) -> Result<()> {
        if !vendor_dir.exists() {
            return Ok(());
        }

        for entry in fs::read_dir(vendor_dir)? {
            let entry = entry?;
            let path = entry.path();

            if !path.is_dir() {
                continue;
            }

            let name = entry.file_name().to_string_lossy().to_string();

            // Skip hidden directories and vendor config
            if name.starts_with('.') {
                continue;
            }

            if !keep.contains(&name) {
                if verbose {
                    println!("   🗑️  Removing unused: {}", name);
                }
                fs::remove_dir_all(&path)
                    .with_context(|| format!("Failed to remove {}", path.display()))?;
            }
        }

        Ok(())
    }

    /// Verify vendor directory integrity
    fn verify_vendor(
        &self,
        vendor_dir: &Path,
        lockfile: &Lockfile,
        vendor_config: Option<&VendorConfig>,
    ) -> Result<()> {
        println!("\n🔍 Verifying vendor directory...");

        let vendor_config = vendor_config.ok_or_else(|| {
            anyhow::anyhow!("No {} found in vendor directory", VENDOR_TOML)
        })?;

        let mut missing = Vec::new();
        let mut outdated = Vec::new();

        for locked_pkg in &lockfile.packages {
            let pkg_dir = vendor_dir.join(&locked_pkg.name);

            if !pkg_dir.exists() {
                missing.push(locked_pkg.name.clone());
                continue;
            }

            if let Some(vendored) = vendor_config.get_package(&locked_pkg.name) {
                if vendored.source != locked_pkg.source {
                    outdated.push(locked_pkg.name.clone());
                }
            } else {
                // In vendor dir but not in config
                missing.push(locked_pkg.name.clone());
            }
        }

        if missing.is_empty() && outdated.is_empty() {
            println!("\n✓ Vendor directory is up-to-date");
            println!("  {} packages verified", lockfile.packages.len());
            Ok(())
        } else {
            println!("\n⚠️  Vendor directory needs update:");
            if !missing.is_empty() {
                println!("   Missing: {}", missing.join(", "));
            }
            if !outdated.is_empty() {
                println!("   Outdated: {}", outdated.join(", "));
            }
            println!("\n   Run 'ccgo vendor --sync' to fix");
            bail!("Vendor verification failed");
        }
    }

    /// Get CCGO home directory
    fn get_ccgo_home() -> PathBuf {
        directories::BaseDirs::new()
            .and_then(|dirs| Some(dirs.home_dir().to_path_buf()))
            .unwrap_or_else(|| PathBuf::from("."))
            .join(".ccgo")
    }
}

/// Check if vendor directory exists and has packages
pub fn has_vendor_dir(project_dir: &Path) -> bool {
    let vendor_dir = project_dir.join(VENDOR_DIR);
    let vendor_toml = vendor_dir.join(VENDOR_TOML);
    vendor_dir.exists() && vendor_toml.exists()
}

/// Get vendored package path if available
pub fn get_vendored_path(project_dir: &Path, package_name: &str) -> Option<PathBuf> {
    let vendor_dir = project_dir.join(VENDOR_DIR);
    let pkg_dir = vendor_dir.join(package_name);

    if pkg_dir.exists() {
        Some(pkg_dir)
    } else {
        None
    }
}
