use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use clap::Parser;
use codex_core::config::Config;
use codex_core::config::find_codex_home;
use codex_core_plugins::ConfiguredMarketplace;
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
use codex_core_plugins::PluginInstallOutcome;
use codex_core_plugins::PluginInstallRequest;
use codex_core_plugins::PluginsConfigInput;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
use codex_core_plugins::marketplace::MarketplaceListError;
use codex_core_plugins::marketplace::MarketplacePluginAuthPolicy;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_cli::CliConfigOverrides;
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;

use crate::marketplace_cmd::MarketplaceCli;

const OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME: &str = "openai-bundled-alpha";
const OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME: &str = "openai-primary-runtime";

#[derive(Debug, Parser)]
#[command(bin_name = "codex plugin")]
pub struct PluginCli {
    #[clap(flatten)]
    pub config_overrides: CliConfigOverrides,

    #[command(subcommand)]
    pub subcommand: PluginSubcommand,
}

#[derive(Debug, clap::Subcommand)]
pub enum PluginSubcommand {
    /// Install a plugin from a configured marketplace snapshot.
    ///
    /// Pass either `PLUGIN@MARKETPLACE` or pass `PLUGIN` with
    /// `--marketplace MARKETPLACE`.
    Add(AddPluginArgs),

    /// List plugins available from configured marketplace snapshots.
    List(ListPluginsArgs),

    /// Add, list, upgrade, or remove configured plugin marketplaces.
    Marketplace(MarketplaceCli),

    /// Remove an installed plugin from local config and cache.
    ///
    /// Pass either `PLUGIN@MARKETPLACE` or pass `PLUGIN` with
    /// `--marketplace MARKETPLACE`.
    Remove(RemovePluginArgs),
}

#[derive(Debug, Parser)]
#[command(
    bin_name = "codex plugin add",
    after_help = "Examples:\n  codex plugin add sample@debug\n  codex plugin add sample --marketplace debug"
)]
pub struct AddPluginArgs {
    /// Plugin selector to install: either PLUGIN@MARKETPLACE or PLUGIN with --marketplace.
    #[arg(value_name = "PLUGIN[@MARKETPLACE]")]
    plugin: String,

    /// Configured marketplace name to use when PLUGIN does not include @MARKETPLACE.
    #[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
    marketplace_name: Option<String>,

    /// Output install result as JSON.
    #[arg(long = "json")]
    json: bool,
}

#[derive(Debug, Parser)]
#[command(
    bin_name = "codex plugin list",
    after_help = "Examples:\n  codex plugin list\n  codex plugin list --marketplace debug\n  codex plugin list --json\n  codex plugin list --available --json"
)]
pub struct ListPluginsArgs {
    /// Only list plugins from this configured marketplace name.
    #[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
    marketplace_name: Option<String>,

    /// Output plugin list as JSON.
    #[arg(long = "json")]
    json: bool,

    /// Include uninstalled marketplace plugins in the JSON output.
    #[arg(long = "available", requires = "json")]
    available: bool,
}

#[derive(Debug, Parser)]
#[command(
    bin_name = "codex plugin remove",
    after_help = "Examples:\n  codex plugin remove sample@debug\n  codex plugin remove sample --marketplace debug"
)]
pub struct RemovePluginArgs {
    /// Plugin selector to remove: either PLUGIN@MARKETPLACE or PLUGIN with --marketplace.
    #[arg(value_name = "PLUGIN[@MARKETPLACE]")]
    plugin: String,

    /// Marketplace name to use when PLUGIN does not include @MARKETPLACE.
    #[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
    marketplace_name: Option<String>,

    /// Output remove result as JSON.
    #[arg(long = "json")]
    json: bool,
}

pub async fn run_plugin_add(
    overrides: Vec<(String, toml::Value)>,
    args: AddPluginArgs,
) -> Result<()> {
    let PluginCommandContext {
        codex_home,
        plugins_input,
        manager,
    } = load_plugin_command_context(overrides).await?;
    let AddPluginArgs {
        plugin,
        marketplace_name,
        json,
    } = args;
    let PluginSelection {
        plugin_name,
        marketplace_name,
        ..
    } = parse_plugin_selection(plugin, marketplace_name)?;
    let marketplace = find_marketplace_for_plugin(
        &manager,
        codex_home.as_path(),
        &plugins_input,
        &marketplace_name,
        &plugin_name,
    )?;
    let outcome = manager
        .install_plugin(PluginInstallRequest {
            plugin_name,
            marketplace_path: marketplace.path,
        })
        .await?;

    if json {
        let output = JsonPluginAddOutput::from_outcome(outcome);
        println!("{}", serde_json::to_string_pretty(&output)?);
        return Ok(());
    }

    println!(
        "Added plugin `{}` from marketplace `{}`.",
        outcome.plugin_id.plugin_name, outcome.plugin_id.marketplace_name
    );
    println!(
        "Installed plugin root: {}",
        outcome.installed_path.as_path().display()
    );

    Ok(())
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginAddOutput {
    plugin_id: String,
    name: String,
    marketplace_name: String,
    version: String,
    installed_path: String,
    auth_policy: &'static str,
}

impl JsonPluginAddOutput {
    fn from_outcome(outcome: PluginInstallOutcome) -> Self {
        Self {
            plugin_id: outcome.plugin_id.as_key(),
            name: outcome.plugin_id.plugin_name,
            marketplace_name: outcome.plugin_id.marketplace_name,
            version: outcome.plugin_version,
            installed_path: outcome.installed_path.as_path().display().to_string(),
            auth_policy: auth_policy_label(outcome.auth_policy),
        }
    }
}

pub async fn run_plugin_list(
    overrides: Vec<(String, toml::Value)>,
    args: ListPluginsArgs,
) -> Result<()> {
    let PluginCommandContext {
        codex_home,
        plugins_input,
        manager,
        ..
    } = load_plugin_command_context(overrides).await?;
    let outcome = manager
        .list_marketplaces_for_config(&plugins_input, &[], /*include_openai_curated*/ true)
        .context("failed to list marketplace plugins")?;
    ensure_configured_marketplace_snapshots_loaded(
        codex_home.as_path(),
        &plugins_input,
        &outcome.errors,
        /*marketplace_name*/ None,
    )?;

    let marketplaces = outcome
        .marketplaces
        .into_iter()
        .filter(|marketplace| {
            args.marketplace_name
                .as_ref()
                .is_none_or(|name| marketplace.name == *name)
        })
        .collect::<Vec<_>>();
    let marketplace_sources = configured_marketplace_sources(&plugins_input);

    if args.json {
        let output = JsonPluginListOutput::from_marketplaces(
            marketplaces,
            args.available,
            &marketplace_sources,
        );
        println!("{}", serde_json::to_string_pretty(&output)?);
        return Ok(());
    }

    if marketplaces.is_empty() {
        if let Some(marketplace_name) = args.marketplace_name {
            println!("No plugins found in marketplace `{marketplace_name}`.");
        } else {
            println!("No marketplace plugins found.");
        }
    } else {
        for (index, marketplace) in marketplaces.into_iter().enumerate() {
            let mut rows = Vec::new();
            let mut plugin_width = "PLUGIN".len();
            let mut status_width = "STATUS".len();
            let mut installed_version_width = "VERSION".len();
            let mut path_width = "PATH".len();

            for plugin in &marketplace.plugins {
                let state = if plugin.installed && plugin.enabled {
                    "installed, enabled"
                } else if plugin.installed {
                    "installed, disabled"
                } else {
                    "not installed"
                };
                let installed_version = plugin.installed_version.clone().unwrap_or_default();
                let path = match &plugin.source {
                    codex_core_plugins::marketplace::MarketplacePluginSource::Local { path } => {
                        path.as_path().display().to_string()
                    }
                    codex_core_plugins::marketplace::MarketplacePluginSource::Git {
                        url,
                        path,
                        ref_name,
                        sha,
                    } => {
                        let mut parts = vec![url.clone()];
                        if let Some(path) = path {
                            parts.push(format!("path `{path}`"));
                        }
                        if let Some(ref_name) = ref_name {
                            parts.push(format!("ref `{ref_name}`"));
                        }
                        if let Some(sha) = sha {
                            parts.push(format!("sha `{sha}`"));
                        }
                        parts.join(", ")
                    }
                };
                plugin_width = plugin_width.max(plugin.id.len());
                status_width = status_width.max(state.len());
                installed_version_width = installed_version_width.max(installed_version.len());
                path_width = path_width.max(path.len());
                rows.push((plugin.id.clone(), state, installed_version, path));
            }

            if index > 0 {
                println!();
            }
            println!("Marketplace `{}`", marketplace.name);
            println!("{}", marketplace.path.as_path().display());
            println!();
            println!(
                "{:<plugin_width$}  {:<status_width$}  {:<installed_version_width$}  {:<path_width$}",
                "PLUGIN", "STATUS", "VERSION", "PATH"
            );
            for (plugin, status, installed_version, path) in rows {
                println!(
                    "{plugin:<plugin_width$}  {status:<status_width$}  {installed_version:<installed_version_width$}  {path:<path_width$}"
                );
            }
        }
    }

    Ok(())
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginListOutput {
    installed: Vec<JsonPluginListEntry>,
    available: Vec<JsonPluginListEntry>,
}

impl JsonPluginListOutput {
    fn from_marketplaces(
        marketplaces: Vec<codex_core_plugins::ConfiguredMarketplace>,
        include_available: bool,
        marketplace_sources: &HashMap<String, JsonMarketplaceSource>,
    ) -> Self {
        let mut installed = Vec::new();
        let mut available = Vec::new();

        for marketplace in marketplaces {
            let marketplace_source = marketplace_sources.get(&marketplace.name).cloned();
            for plugin in marketplace.plugins {
                let entry = JsonPluginListEntry::from_configured_plugin(
                    &marketplace.name,
                    marketplace_source.clone(),
                    plugin,
                );
                if entry.installed {
                    installed.push(entry);
                } else if include_available {
                    available.push(entry);
                }
            }
        }

        Self {
            installed,
            available,
        }
    }
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginListEntry {
    plugin_id: String,
    name: String,
    marketplace_name: String,
    version: Option<String>,
    installed: bool,
    enabled: bool,
    source: JsonPluginSource,
    #[serde(skip_serializing_if = "Option::is_none")]
    marketplace_source: Option<JsonMarketplaceSource>,
    install_policy: &'static str,
    auth_policy: &'static str,
}

impl JsonPluginListEntry {
    fn from_configured_plugin(
        marketplace_name: &str,
        marketplace_source: Option<JsonMarketplaceSource>,
        plugin: codex_core_plugins::ConfiguredMarketplacePlugin,
    ) -> Self {
        let version = plugin.installed_version.or(plugin.local_version);
        Self {
            plugin_id: plugin.id,
            name: plugin.name,
            marketplace_name: marketplace_name.to_string(),
            version,
            installed: plugin.installed,
            enabled: plugin.enabled,
            source: JsonPluginSource::from_marketplace_source(plugin.source),
            marketplace_source,
            install_policy: install_policy_label(plugin.policy.installation),
            auth_policy: auth_policy_label(plugin.policy.authentication),
        }
    }
}

#[derive(Debug, Serialize)]
#[serde(tag = "source", rename_all = "kebab-case")]
enum JsonPluginSource {
    Local {
        path: String,
    },
    Git {
        url: String,
        #[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
        ref_name: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        sha: Option<String>,
    },
    GitSubdir {
        url: String,
        path: String,
        #[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
        ref_name: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        sha: Option<String>,
    },
}

impl JsonPluginSource {
    fn from_marketplace_source(source: MarketplacePluginSource) -> Self {
        match source {
            MarketplacePluginSource::Local { path } => Self::Local {
                path: path.as_path().display().to_string(),
            },
            MarketplacePluginSource::Git {
                url,
                path: Some(path),
                ref_name,
                sha,
            } => Self::GitSubdir {
                url,
                path,
                ref_name,
                sha,
            },
            MarketplacePluginSource::Git {
                url,
                path: None,
                ref_name,
                sha,
            } => Self::Git { url, ref_name, sha },
        }
    }
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct JsonMarketplaceSource {
    source_type: String,
    source: String,
}

pub(crate) fn configured_marketplace_sources(
    plugins_input: &PluginsConfigInput,
) -> HashMap<String, JsonMarketplaceSource> {
    let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else {
        return HashMap::new();
    };
    let Some(marketplaces) = user_config
        .get("marketplaces")
        .and_then(toml::Value::as_table)
    else {
        return HashMap::new();
    };

    marketplaces
        .iter()
        .filter_map(|(marketplace_name, marketplace)| {
            let source_type = marketplace
                .get("source_type")
                .and_then(toml::Value::as_str)?;
            let source = marketplace.get("source").and_then(toml::Value::as_str)?;
            Some((
                marketplace_name.clone(),
                JsonMarketplaceSource {
                    source_type: source_type.to_string(),
                    source: source.to_string(),
                },
            ))
        })
        .collect()
}

fn install_policy_label(policy: MarketplacePluginInstallPolicy) -> &'static str {
    match policy {
        MarketplacePluginInstallPolicy::NotAvailable => "NOT_AVAILABLE",
        MarketplacePluginInstallPolicy::Available => "AVAILABLE",
        MarketplacePluginInstallPolicy::InstalledByDefault => "INSTALLED_BY_DEFAULT",
    }
}

fn auth_policy_label(policy: MarketplacePluginAuthPolicy) -> &'static str {
    match policy {
        MarketplacePluginAuthPolicy::OnInstall => "ON_INSTALL",
        MarketplacePluginAuthPolicy::OnUse => "ON_USE",
    }
}

pub async fn run_plugin_remove(
    overrides: Vec<(String, toml::Value)>,
    args: RemovePluginArgs,
) -> Result<()> {
    let PluginCommandContext { manager, .. } = load_plugin_command_context(overrides).await?;
    let RemovePluginArgs {
        plugin,
        marketplace_name,
        json,
    } = args;
    let selection = parse_plugin_selection(plugin, marketplace_name)?;

    manager
        .uninstall_plugin(selection.plugin_key.clone())
        .await?;
    if json {
        let output = JsonPluginRemoveOutput::from_selection(selection);
        println!("{}", serde_json::to_string_pretty(&output)?);
        return Ok(());
    }

    println!(
        "Removed plugin `{}` from marketplace `{}`.",
        selection.plugin_name, selection.marketplace_name
    );

    Ok(())
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginRemoveOutput {
    plugin_id: String,
    name: String,
    marketplace_name: String,
}

impl JsonPluginRemoveOutput {
    fn from_selection(selection: PluginSelection) -> Self {
        Self {
            plugin_id: selection.plugin_key,
            name: selection.plugin_name,
            marketplace_name: selection.marketplace_name,
        }
    }
}

struct PluginCommandContext {
    codex_home: PathBuf,
    plugins_input: PluginsConfigInput,
    manager: PluginsManager,
}

async fn load_plugin_command_context(
    overrides: Vec<(String, toml::Value)>,
) -> Result<PluginCommandContext> {
    let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
    let config = Config::load_with_cli_overrides(overrides)
        .await
        .context("failed to load configuration")?;
    let plugins_input = config.plugins_config_input();
    let manager = PluginsManager::new(codex_home.to_path_buf());
    Ok(PluginCommandContext {
        codex_home: codex_home.to_path_buf(),
        plugins_input,
        manager,
    })
}

struct PluginSelection {
    plugin_name: String,
    marketplace_name: String,
    plugin_key: String,
}

impl PluginSelection {
    fn from_plugin_id(plugin_id: PluginId) -> Self {
        let plugin_key = plugin_id.as_key();
        Self {
            plugin_name: plugin_id.plugin_name,
            marketplace_name: plugin_id.marketplace_name,
            plugin_key,
        }
    }
}

fn parse_plugin_selection(
    plugin: String,
    marketplace_name: Option<String>,
) -> Result<PluginSelection> {
    match (PluginId::parse(&plugin), marketplace_name) {
        (Ok(plugin_id), None) => Ok(PluginSelection::from_plugin_id(plugin_id)),
        (Ok(plugin_id), Some(marketplace_name)) => {
            if plugin_id.marketplace_name != marketplace_name {
                bail!(
                    "plugin id `{}` belongs to marketplace `{}`, but --marketplace specified `{}`",
                    plugin,
                    plugin_id.marketplace_name,
                    marketplace_name
                );
            }
            Ok(PluginSelection::from_plugin_id(plugin_id))
        }
        (Err(_), Some(marketplace_name)) => Ok(PluginSelection::from_plugin_id(PluginId::new(
            plugin,
            marketplace_name,
        )?)),
        (Err(_), None) => {
            bail!("plugin requires --marketplace unless passed as <plugin>@<marketplace>")
        }
    }
}

fn find_marketplace_for_plugin(
    manager: &PluginsManager,
    codex_home: &std::path::Path,
    plugins_input: &PluginsConfigInput,
    marketplace_name: &str,
    plugin_name: &str,
) -> Result<ConfiguredMarketplace> {
    let outcome = manager
        .list_marketplaces_for_config(plugins_input, &[], /*include_openai_curated*/ true)
        .context("failed to list marketplace plugins")?;
    ensure_configured_marketplace_snapshots_loaded(
        codex_home,
        plugins_input,
        &outcome.errors,
        Some(marketplace_name),
    )?;
    let matches = outcome
        .marketplaces
        .into_iter()
        .filter(|marketplace| marketplace.name == marketplace_name)
        .filter(|marketplace| {
            marketplace
                .plugins
                .iter()
                .any(|plugin| plugin.name == plugin_name)
        })
        .collect::<Vec<_>>();

    match matches.as_slice() {
        [] => bail!("plugin `{plugin_name}` was not found in marketplace `{marketplace_name}`"),
        [marketplace] => Ok(marketplace.clone()),
        _ => bail!(
            "plugin `{plugin_name}` in marketplace `{marketplace_name}` matched multiple marketplace roots"
        ),
    }
}

pub(crate) struct ConfiguredMarketplaceSnapshotIssue {
    pub(crate) marketplace_name: String,
    pub(crate) path: PathBuf,
    pub(crate) message: String,
}

fn ensure_configured_marketplace_snapshots_loaded(
    codex_home: &std::path::Path,
    plugins_input: &PluginsConfigInput,
    load_errors: &[MarketplaceListError],
    marketplace_name: Option<&str>,
) -> Result<()> {
    let issues = configured_marketplace_snapshot_issues(
        codex_home,
        plugins_input,
        load_errors,
        marketplace_name,
    );
    if issues.is_empty() {
        return Ok(());
    }

    let issue_lines = issues
        .iter()
        .map(|issue| {
            format!(
                "- `{}` at {}: {}",
                issue.marketplace_name,
                issue.path.display(),
                issue.message
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    bail!("failed to load configured marketplace snapshot(s):\n{issue_lines}");
}

pub(crate) fn configured_marketplace_snapshot_issues(
    codex_home: &std::path::Path,
    plugins_input: &PluginsConfigInput,
    load_errors: &[MarketplaceListError],
    marketplace_name: Option<&str>,
) -> Vec<ConfiguredMarketplaceSnapshotIssue> {
    let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else {
        return Vec::new();
    };
    let Some(configured_marketplaces) = user_config
        .get("marketplaces")
        .and_then(toml::Value::as_table)
    else {
        return Vec::new();
    };

    let default_install_root = marketplace_install_root(codex_home);
    let mut manifest_paths = Vec::new();
    let mut issues = Vec::new();
    for (configured_name, marketplace) in configured_marketplaces {
        if marketplace_name.is_some_and(|name| configured_name != name) {
            continue;
        }
        if !marketplace.is_table() {
            issues.push(ConfiguredMarketplaceSnapshotIssue {
                marketplace_name: configured_name.clone(),
                path: PathBuf::from("<invalid config>"),
                message: "configured marketplace entry must be a table".to_string(),
            });
            continue;
        }
        if let Err(err) = validate_plugin_segment(configured_name, "marketplace name") {
            issues.push(ConfiguredMarketplaceSnapshotIssue {
                marketplace_name: configured_name.clone(),
                path: PathBuf::from("<invalid config>"),
                message: err.to_string(),
            });
            continue;
        }
        if marketplace.get("source_type").and_then(toml::Value::as_str) == Some("local")
            && marketplace
                .get("source")
                .and_then(toml::Value::as_str)
                .is_none_or(str::is_empty)
        {
            issues.push(ConfiguredMarketplaceSnapshotIssue {
                marketplace_name: configured_name.clone(),
                path: PathBuf::from("<invalid source>"),
                message: "configured local marketplace source is missing or empty".to_string(),
            });
            continue;
        }
        let Some(root) = resolve_configured_marketplace_root(
            configured_name,
            marketplace,
            &default_install_root,
        ) else {
            continue;
        };
        match find_marketplace_manifest_path(&root) {
            Some(path) => manifest_paths.push((configured_name.clone(), path)),
            None => {
                if is_implicit_system_marketplace_root(configured_name, codex_home, &root) {
                    continue;
                }
                issues.push(ConfiguredMarketplaceSnapshotIssue {
                    marketplace_name: configured_name.clone(),
                    path: root,
                    message: "marketplace root does not contain a supported manifest".to_string(),
                });
            }
        }
    }

    for error in load_errors {
        if let Some((configured_name, _)) = manifest_paths
            .iter()
            .find(|(_, path)| path.as_path() == error.path.as_path())
        {
            issues.push(ConfiguredMarketplaceSnapshotIssue {
                marketplace_name: configured_name.clone(),
                path: error.path.to_path_buf(),
                message: error.message.clone(),
            });
        }
    }
    issues
}

fn is_implicit_system_marketplace_root(
    marketplace_name: &str,
    _codex_home: &Path,
    root: &Path,
) -> bool {
    if matches!(
        marketplace_name,
        OPENAI_BUNDLED_MARKETPLACE_NAME | OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME
    ) && path_ends_with(root, &[".tmp", "bundled-marketplaces", marketplace_name])
    {
        return true;
    }

    marketplace_name == OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME
        && path_ends_with(
            root,
            &[
                "codex-runtimes",
                "codex-primary-runtime",
                "plugins",
                marketplace_name,
            ],
        )
}

fn path_ends_with(path: &Path, suffix: &[&str]) -> bool {
    let path_components = path
        .components()
        .map(|component| component.as_os_str().to_string_lossy().into_owned())
        .collect::<Vec<_>>();
    path_components.as_slice().ends_with(
        &suffix
            .iter()
            .map(std::string::ToString::to_string)
            .collect::<Vec<_>>(),
    )
}
