use anyhow::Context as _; // brings with_context into scope without name conflict
use anyhow::{anyhow, Result};
use chrono::Local;
use std::fs;
use std::path::PathBuf;

use crate::cli::*;
use crate::config::PipConfig;
use crate::discover;
use crate::display;
use crate::error::PipConfError;
use crate::schema;

/// Shared context passed to every command
pub struct Context {
    pub config_file: Option<String>,
    pub output_format: OutputFormat,
    pub no_color: bool,
    pub verbose: bool,
}

impl Context {
    /// Resolve the config file path to use
    pub fn resolve_config_path(&self) -> PathBuf {
        if let Some(ref f) = self.config_file {
            return PathBuf::from(f);
        }
        discover::default_write_path()
    }

    /// Load config (from override path or discovered path)
    pub fn load_config(&self) -> Result<PipConfig> {
        let path = self.resolve_config_path();
        if path.exists() {
            PipConfig::from_file(&path)
        } else {
            Ok(PipConfig::new())
        }
    }

    /// Load config and require it to exist
    pub fn load_config_required(&self) -> Result<PipConfig> {
        let path = self.resolve_config_path();
        if !path.exists() {
            return Err(PipConfError::ConfigNotFound(path.display().to_string()).into());
        }
        PipConfig::from_file(&path)
    }

    pub fn verbose_path(&self) {
        if self.verbose {
            let path = self.resolve_config_path();
            display::info(&format!("Using config: {}", path.display()), self.no_color);
        }
    }
}

// ─── discover ────────────────────────────────────────────────────────────────

pub fn cmd_discover(ctx: &Context, args: &DiscoverArgs) -> Result<()> {
    let mut locs = discover::discover_all();
    if args.existing_only {
        locs.retain(|l| l.exists);
    }
    display::print_locations(&locs, ctx.no_color);
    Ok(())
}

// ─── list ────────────────────────────────────────────────────────────────────

pub fn cmd_list(ctx: &Context, args: &ListArgs) -> Result<()> {
    ctx.verbose_path();
    let cfg = ctx.load_config()?;

    let display_cfg = if let Some(ref sec) = args.section {
        let mut filtered = PipConfig::new();
        if let Some(keys) = cfg.sections.get(sec) {
            filtered.sections.insert(sec.clone(), keys.clone());
        } else {
            display::warn(&format!("Section [{}] not found", sec), ctx.no_color);
            return Ok(());
        }
        filtered
    } else {
        cfg
    };

    display::print_config(&display_cfg, &ctx.output_format, ctx.no_color);
    Ok(())
}

// ─── get ─────────────────────────────────────────────────────────────────────

pub fn cmd_get(ctx: &Context, args: &GetArgs) -> Result<()> {
    ctx.verbose_path();
    let cfg = ctx.load_config_required()?;
    match cfg.get(&args.section, &args.key) {
        Some(val) => println!("{}", val),
        None => {
            return Err(PipConfError::KeyNotFound {
                section: args.section.clone(),
                key: args.key.clone(),
            }
            .into());
        }
    }
    Ok(())
}

// ─── set ─────────────────────────────────────────────────────────────────────

pub fn cmd_set(ctx: &Context, args: &SetArgs) -> Result<()> {
    let path = ctx.resolve_config_path();
    ctx.verbose_path();

    // Validate the key against schema (non-fatal in non-strict mode)
    if schema::find_key(&args.section, &args.key).is_none() {
        display::warn(
            &format!("'{}' is not a known pip config key for [{}]. Use 'pipconf schema' to list valid keys.", args.key, args.section),
            ctx.no_color,
        );
    }

    let mut cfg = if path.exists() {
        PipConfig::from_file(&path)?
    } else {
        PipConfig::new()
    };

    cfg.set(&args.section, &args.key, &args.value);
    cfg.write_to_file(&path)?;

    display::success(
        &format!(
            "[{}] {} = {} (saved to {})",
            args.section,
            args.key,
            args.value,
            path.display()
        ),
        ctx.no_color,
    );
    Ok(())
}

// ─── unset ───────────────────────────────────────────────────────────────────

pub fn cmd_unset(ctx: &Context, args: &UnsetArgs) -> Result<()> {
    let path = ctx.resolve_config_path();
    ctx.verbose_path();

    let mut cfg = PipConfig::from_file(&path)?;
    if cfg.unset(&args.section, &args.key) {
        cfg.write_to_file(&path)?;
        display::success(
            &format!(
                "Removed [{}] {} from {}",
                args.section,
                args.key,
                path.display()
            ),
            ctx.no_color,
        );
    } else {
        return Err(PipConfError::KeyNotFound {
            section: args.section.clone(),
            key: args.key.clone(),
        }
        .into());
    }
    Ok(())
}

// ─── add ─────────────────────────────────────────────────────────────────────

pub fn cmd_add(ctx: &Context, args: &AddArgs) -> Result<()> {
    let path = ctx.resolve_config_path();
    let mut cfg = if path.exists() {
        PipConfig::from_file(&path)?
    } else {
        PipConfig::new()
    };
    cfg.add_to_list(&args.section, &args.key, &args.value);
    cfg.write_to_file(&path)?;
    display::success(
        &format!("Added '{}' to [{}] {}", args.value, args.section, args.key),
        ctx.no_color,
    );
    Ok(())
}

// ─── remove ──────────────────────────────────────────────────────────────────

pub fn cmd_remove(ctx: &Context, args: &RemoveArgs) -> Result<()> {
    let path = ctx.resolve_config_path();
    let mut cfg = PipConfig::from_file(&path)?;
    if cfg.remove_from_list(&args.section, &args.key, &args.value) {
        cfg.write_to_file(&path)?;
        display::success(
            &format!(
                "Removed '{}' from [{}] {}",
                args.value, args.section, args.key
            ),
            ctx.no_color,
        );
    } else {
        display::warn(
            &format!(
                "'{}' not found in [{}] {}",
                args.value, args.section, args.key
            ),
            ctx.no_color,
        );
    }
    Ok(())
}

// ─── validate ────────────────────────────────────────────────────────────────

pub fn cmd_validate(ctx: &Context, args: &ValidateArgs) -> Result<()> {
    let path = ctx.resolve_config_path();
    ctx.verbose_path();

    if !path.exists() {
        display::warn("No config file found to validate.", ctx.no_color);
        return Ok(());
    }

    let cfg = PipConfig::from_file(&path)?;
    let known_keys = schema::all_keys();
    let valid_sections = schema::sections();

    let mut errors: Vec<String> = vec![];
    let mut warnings: Vec<String> = vec![];

    for (section, keys) in &cfg.sections {
        if !valid_sections.contains(&section.as_str()) {
            warnings.push(format!("Unknown section [{}]", section));
            continue;
        }
        for (key, value) in keys {
            let ks = known_keys
                .iter()
                .find(|k| k.section == section && k.key == key);
            if let Some(schema) = ks {
                // Type validation
                match schema.value_type {
                    schema::ValueType::Bool => {
                        if !["true", "false", "1", "0", "yes", "no"]
                            .contains(&value.to_lowercase().as_str())
                        {
                            errors.push(format!(
                                "[{}] {}: '{}' is not a valid boolean (true/false)",
                                section, key, value
                            ));
                        }
                    }
                    schema::ValueType::Integer => {
                        if value.parse::<i64>().is_err() {
                            errors.push(format!(
                                "[{}] {}: '{}' is not a valid integer",
                                section, key, value
                            ));
                        }
                    }
                    schema::ValueType::Url => {
                        // Basic URL check
                        if !value.starts_with("http://")
                            && !value.starts_with("https://")
                            && !value.starts_with("file://")
                        {
                            warnings.push(format!(
                                "[{}] {}: '{}' may not be a valid URL",
                                section, key, value
                            ));
                        }
                    }
                    _ => {}
                }
                if schema.deprecated {
                    warnings.push(format!(
                        "[{}] {}: DEPRECATED. {}",
                        section,
                        key,
                        schema
                            .deprecated_msg
                            .unwrap_or("Please consult pip documentation.")
                    ));
                }
            } else if args.strict {
                warnings.push(format!("[{}] {}: unknown key", section, key));
            }
        }
    }

    if errors.is_empty() && warnings.is_empty() {
        display::success("Config is valid.", ctx.no_color);
    } else {
        for e in &errors {
            display::error_msg(&format!("ERROR: {}", e), ctx.no_color);
        }
        for w in &warnings {
            display::warn(&format!("WARN: {}", w), ctx.no_color);
        }
        if !errors.is_empty() {
            return Err(anyhow!("{} validation error(s) found.", errors.len()));
        }
    }
    Ok(())
}

// ─── backup ──────────────────────────────────────────────────────────────────

pub fn cmd_backup(ctx: &Context, args: &BackupArgs) -> Result<()> {
    let src = ctx.resolve_config_path();
    if !src.exists() {
        return Err(PipConfError::ConfigNotFound(src.display().to_string()).into());
    }

    let dest = if let Some(ref d) = args.dest {
        PathBuf::from(d)
    } else {
        let ts = Local::now().format("%Y%m%d_%H%M%S");
        let fname = format!(
            "{}.bak.{}",
            src.file_name().unwrap_or_default().to_string_lossy(),
            ts
        );
        src.parent().unwrap_or(&PathBuf::from(".")).join(fname)
    };

    fs::copy(&src, &dest)
        .with_context(|| format!("Backup failed: {} -> {}", src.display(), dest.display()))?;

    display::success(&format!("Backup saved to {}", dest.display()), ctx.no_color);
    Ok(())
}

// ─── restore ─────────────────────────────────────────────────────────────────

pub fn cmd_restore(ctx: &Context, args: &RestoreArgs) -> Result<()> {
    let backup = PathBuf::from(&args.backup_file);
    if !backup.exists() {
        return Err(PipConfError::ConfigNotFound(backup.display().to_string()).into());
    }

    let dest = ctx.resolve_config_path();

    // Validate the backup is parseable
    PipConfig::from_file(&backup)
        .with_context(|| format!("Backup file is invalid: {}", backup.display()))?;

    // Auto-backup current before restoring
    if dest.exists() {
        let ts = Local::now().format("%Y%m%d_%H%M%S");
        let auto_bak = dest.with_extension(format!("pre-restore.{}", ts));
        fs::copy(&dest, &auto_bak)?;
        display::info(
            &format!("Current config backed up to {}", auto_bak.display()),
            ctx.no_color,
        );
    }

    fs::copy(&backup, &dest)?;
    display::success(&format!("Restored from {}", backup.display()), ctx.no_color);
    Ok(())
}

// ─── edit ────────────────────────────────────────────────────────────────────

pub fn cmd_edit(ctx: &Context, args: &EditArgs) -> Result<()> {
    let path = ctx.resolve_config_path();

    // Create file if not existing
    if !path.exists() {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&path, "[global]\n")?;
    }

    let editor = args
        .editor
        .clone()
        .or_else(|| std::env::var("VISUAL").ok())
        .or_else(|| std::env::var("EDITOR").ok())
        .unwrap_or_else(|| {
            #[cfg(target_os = "windows")]
            {
                "notepad".to_string()
            }
            #[cfg(not(target_os = "windows"))]
            {
                "vi".to_string()
            }
        });

    let status = std::process::Command::new(&editor)
        .arg(&path)
        .status()
        .with_context(|| format!("Failed to launch editor: {}", editor))?;

    if !status.success() {
        return Err(PipConfError::EditorError(format!(
            "{} exited with code {:?}",
            editor,
            status.code()
        ))
        .into());
    }

    // Validate after edit
    match PipConfig::from_file(&path) {
        Ok(_) => display::success("Config saved and is valid.", ctx.no_color),
        Err(e) => display::warn(
            &format!("Config saved but has parse issues: {}", e),
            ctx.no_color,
        ),
    }
    Ok(())
}

// ─── path ────────────────────────────────────────────────────────────────────

pub fn cmd_path(_ctx: &Context, args: &PathArgs) -> Result<()> {
    match args.level {
        ConfigLevel::All => {
            let locs = discover::discover_all();
            for loc in &locs {
                println!("{}", loc.path.display());
            }
        }
        ConfigLevel::User => {
            if let Some(p) = discover::active_user_config() {
                println!("{}", p.display());
            }
        }
        ConfigLevel::System => {
            let locs = discover::discover_all();
            for loc in locs
                .iter()
                .filter(|l| l.level == discover::ConfigLevel::System)
            {
                println!("{}", loc.path.display());
            }
        }
        ConfigLevel::Site => {
            let locs = discover::discover_all();
            for loc in locs
                .iter()
                .filter(|l| l.level == discover::ConfigLevel::Site)
            {
                println!("{}", loc.path.display());
            }
        }
    }
    Ok(())
}

// ─── schema ──────────────────────────────────────────────────────────────────

pub fn cmd_schema(ctx: &Context) -> Result<()> {
    let keys = schema::all_keys();
    display::print_schema(&keys, ctx.no_color);
    println!(
        "Total: {} known keys across {} sections\n",
        keys.len(),
        schema::sections().len()
    );
    Ok(())
}

// ─── init ────────────────────────────────────────────────────────────────────

pub fn cmd_init(ctx: &Context, args: &InitArgs) -> Result<()> {
    let path = ctx.resolve_config_path();

    if path.exists() && !args.force {
        return Err(anyhow!(
            "Config already exists at {}. Use --force to overwrite.",
            path.display()
        ));
    }

    let mut cfg = PipConfig::new();

    let index_url = args
        .index_url
        .clone()
        .unwrap_or_else(|| "https://pypi.org/simple".to_string());
    cfg.set("global", "index-url", &index_url);
    cfg.set("global", "timeout", "60");
    cfg.set("global", "retries", "5");
    cfg.set("global", "disable-pip-version-check", "true");

    for host in &args.trusted_host {
        cfg.add_to_list("global", "trusted-host", host);
    }

    // If non-PyPI URL given without trusted-host, try to auto-extract host
    if args.index_url.is_some() && args.trusted_host.is_empty() {
        if let Ok(url) = url::Url::parse(&index_url) {
            if let Some(host) = url.host_str() {
                if !host.contains("pypi.org") {
                    cfg.add_to_list("global", "trusted-host", host);
                }
            }
        }
    }

    cfg.write_to_file(&path)?;
    display::success(
        &format!("Initialized pip config at {}", path.display()),
        ctx.no_color,
    );
    display::print_config(&cfg, &ctx.output_format, ctx.no_color);
    Ok(())
}

// ─── reset ───────────────────────────────────────────────────────────────────

pub fn cmd_reset(ctx: &Context, args: &ResetArgs) -> Result<()> {
    let path = ctx.resolve_config_path();
    if !path.exists() {
        display::warn("No config file found.", ctx.no_color);
        return Ok(());
    }

    if !args.yes {
        use dialoguer::Confirm;
        let prompt = if let Some(ref sec) = args.section {
            format!("Reset section [{}] in {}?", sec, path.display())
        } else {
            format!("Reset ALL config in {}?", path.display())
        };
        let confirmed = Confirm::new().with_prompt(prompt).interact()?;
        if !confirmed {
            display::info("Aborted.", ctx.no_color);
            return Ok(());
        }
    }

    let mut cfg = PipConfig::from_file(&path)?;
    if let Some(ref sec) = args.section {
        cfg.sections.shift_remove(sec);
        display::success(&format!("Section [{}] reset.", sec), ctx.no_color);
    } else {
        cfg.sections.clear();
        display::success("All config reset.", ctx.no_color);
    }
    cfg.write_to_file(&path)?;
    Ok(())
}

// ─── merge ───────────────────────────────────────────────────────────────────

pub fn cmd_merge(ctx: &Context, args: &MergeArgs) -> Result<()> {
    let dest_path = ctx.resolve_config_path();
    let src_path = PathBuf::from(&args.source);

    if !src_path.exists() {
        return Err(PipConfError::ConfigNotFound(src_path.display().to_string()).into());
    }

    let src = PipConfig::from_file(&src_path)?;
    let mut dest = if dest_path.exists() {
        PipConfig::from_file(&dest_path)?
    } else {
        PipConfig::new()
    };

    let overwrite = match args.conflict {
        ConflictStrategy::Overwrite => true,
        ConflictStrategy::Keep => false,
        ConflictStrategy::Ask => {
            use dialoguer::Select;
            let choice = Select::new()
                .with_prompt("Conflict resolution strategy")
                .items(&["Keep existing values", "Overwrite with source values"])
                .default(0)
                .interact()?;
            choice == 1
        }
    };

    dest.merge_from(&src, overwrite);
    dest.write_to_file(&dest_path)?;
    display::success(
        &format!("Merged {} into {}", src_path.display(), dest_path.display()),
        ctx.no_color,
    );
    Ok(())
}

// ─── diff ────────────────────────────────────────────────────────────────────

pub fn cmd_diff(ctx: &Context, args: &DiffArgs) -> Result<()> {
    let (path_a, label_a) = if let Some(ref a) = args.file_a {
        (PathBuf::from(a), a.as_str().to_string())
    } else {
        let p = ctx.resolve_config_path();
        let l = p.display().to_string();
        (p, l)
    };
    let path_b = PathBuf::from(&args.file_b);

    let cfg_a = PipConfig::from_file(&path_a)
        .with_context(|| format!("Cannot read file A: {}", path_a.display()))?;
    let cfg_b = PipConfig::from_file(&path_b)
        .with_context(|| format!("Cannot read file B: {}", path_b.display()))?;

    display::print_diff(&label_a, &cfg_a, &args.file_b, &cfg_b, ctx.no_color);
    Ok(())
}

// ─── export ──────────────────────────────────────────────────────────────────

pub fn cmd_export(ctx: &Context, args: &ExportArgs) -> Result<()> {
    ctx.verbose_path();
    let cfg = ctx.load_config_required()?;

    let content = match args.format {
        ExportFormat::Ini => cfg.to_ini(),
        ExportFormat::Json => serde_json::to_string_pretty(&cfg.to_json())?,
        ExportFormat::Toml => {
            let mut out = String::new();
            for (section, keys) in &cfg.sections {
                out.push_str(&format!("[{}]\n", section));
                for (k, v) in keys {
                    let tk = k.replace('-', "_");
                    if v == "true" || v == "false" {
                        out.push_str(&format!("{} = {}\n", tk, v));
                    } else {
                        out.push_str(&format!("{} = \"{}\"\n", tk, v));
                    }
                }
                out.push('\n');
            }
            out
        }
        ExportFormat::Env | ExportFormat::Shell => cfg.to_env_exports(),
    };

    if let Some(ref outfile) = args.output {
        fs::write(outfile, &content)?;
        display::success(&format!("Exported to {}", outfile), ctx.no_color);
    } else {
        print!("{}", content);
    }
    Ok(())
}

// ─── import ──────────────────────────────────────────────────────────────────

pub fn cmd_import(ctx: &Context, args: &ImportArgs) -> Result<()> {
    let content = if args.input == "-" {
        use std::io::Read;
        let mut buf = String::new();
        std::io::stdin().read_to_string(&mut buf)?;
        buf
    } else {
        fs::read_to_string(&args.input)
            .with_context(|| format!("Cannot read import file: {}", args.input))?
    };

    // Detect format
    let format = args.format.clone().unwrap_or_else(|| {
        if content.trim_start().starts_with('{') {
            ExportFormat::Json
        } else if args.input.ends_with(".toml") {
            ExportFormat::Toml
        } else {
            ExportFormat::Ini
        }
    });

    let cfg = match format {
        ExportFormat::Ini => PipConfig::parse(&content, &args.input)?,
        ExportFormat::Json => {
            let v: serde_json::Value = serde_json::from_str(&content)?;
            let mut cfg = PipConfig::new();
            if let serde_json::Value::Object(map) = v {
                for (section, keys) in map {
                    if let serde_json::Value::Object(kmap) = keys {
                        for (key, val) in kmap {
                            let val_str = match val {
                                serde_json::Value::String(s) => s,
                                serde_json::Value::Bool(b) => b.to_string(),
                                serde_json::Value::Number(n) => n.to_string(),
                                serde_json::Value::Array(arr) => arr
                                    .iter()
                                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                                    .collect::<Vec<_>>()
                                    .join("\n"),
                                _ => continue,
                            };
                            cfg.set(&section, &key, &val_str);
                        }
                    }
                }
            }
            cfg
        }
        ExportFormat::Toml => {
            return Err(anyhow!(
                "TOML import is not yet supported. Please use INI or JSON format."
            ));
        }
        ExportFormat::Env | ExportFormat::Shell => {
            return Err(anyhow!("Shell/env export format cannot be imported back."));
        }
    };

    let dest = ctx.resolve_config_path();
    cfg.write_to_file(&dest)?;
    display::success(
        &format!("Imported config to {}", dest.display()),
        ctx.no_color,
    );
    Ok(())
}

// ─── env ─────────────────────────────────────────────────────────────────────

pub fn cmd_env(ctx: &Context) -> Result<()> {
    let vars = schema::pip_env_vars();
    display::print_env_vars(&vars, ctx.no_color);
    Ok(())
}
