use clap::{Args, Subcommand};
use serde::Serialize;

use homeboy::extension::{
    self, extension_ready_status, is_extension_linked, load_extension, run_setup, ExtensionSummary,
    UpdateEntry,
};
use homeboy::project::{self, Project};
use std::path::Path;

use crate::commands::CmdResult;

#[derive(Args)]
pub struct ExtensionArgs {
    #[command(subcommand)]
    command: ExtensionCommand,
}

#[derive(Subcommand)]
enum ExtensionCommand {
    /// Show available extensions with compatibility status
    List {
        /// Project ID to filter compatible extensions
        #[arg(short, long)]
        project: Option<String>,
    },
    /// Show detailed information about a extension
    Show {
        /// Extension ID
        extension_id: String,
    },
    /// Execute a extension
    Run {
        /// Extension ID
        extension_id: String,
        /// Project ID (defaults to active project)
        #[arg(short, long)]
        project: Option<String>,
        /// Component ID (required when ambiguous)
        #[arg(short, long)]
        component: Option<String>,
        /// Input values as key=value pairs
        #[arg(short, long, value_parser = super::parse_key_val)]
        input: Vec<(String, String)>,
        /// Run only specific steps (comma-separated, e.g. --step phpunit,phpcs)
        #[arg(long)]
        step: Option<String>,
        /// Skip specific steps (comma-separated, e.g. --skip phpstan,lint)
        #[arg(long)]
        skip: Option<String>,
        /// Arguments to pass to the extension (for CLI extensions)
        #[arg(trailing_var_arg = true)]
        args: Vec<String>,
        /// Stream output directly to terminal (default: auto-detect based on TTY)
        #[arg(long)]
        stream: bool,
        /// Disable streaming and capture output (default: auto-detect based on TTY)
        #[arg(long)]
        no_stream: bool,
    },
    /// Run the extension's setup command (if defined)
    Setup {
        /// Extension ID
        extension_id: String,
    },
    /// Install a extension from a git URL or local path
    Install {
        /// Git URL or local path to extension directory
        source: String,
        /// Override extension id
        #[arg(long)]
        id: Option<String>,
    },
    /// Install every extension configured by a component
    InstallForComponent {
        /// Git URL or local path to extension repository/directory
        #[arg(long)]
        source: String,
        /// Component path containing homeboy.json (defaults to current directory)
        #[arg(long)]
        path: Option<String>,
    },
    /// Update an installed extension (git pull)
    Update {
        /// Extension ID (omit with --all to update everything)
        extension_id: Option<String>,
        /// Update all installed extensions
        #[arg(long)]
        all: bool,
        /// Force update even with uncommitted changes
        #[arg(long)]
        force: bool,
    },
    /// Uninstall a extension
    Uninstall {
        /// Extension ID
        extension_id: String,
    },
    /// Execute a extension action (API call or builtin)
    Action {
        /// Extension ID
        extension_id: String,
        /// Action ID
        action_id: String,
        /// Project ID (required for API actions)
        #[arg(short, long)]
        project: Option<String>,
        /// JSON array of selected data rows
        #[arg(long)]
        data: Option<String>,
    },
    /// Run a tool from a extension's vendor directory
    Exec {
        /// Extension ID
        extension_id: String,
        /// Component ID (sets working directory to component path)
        #[arg(short, long)]
        component: Option<String>,
        /// Command and arguments to run
        #[arg(trailing_var_arg = true, required = true)]
        args: Vec<String>,
    },
    /// Update extension manifest fields
    #[command(visible_aliases = ["edit", "merge"])]
    Set {
        /// Extension ID (optional if provided in JSON body)
        extension_id: Option<String>,
        /// JSON object to merge into manifest (supports @file and - for stdin)
        #[arg(long, value_name = "JSON")]
        json: String,
        /// Replace these fields instead of merging arrays
        #[arg(long, value_name = "FIELD")]
        replace: Vec<String>,
    },
}

pub fn run(
    args: ExtensionArgs,
    _global: &crate::commands::GlobalArgs,
) -> CmdResult<ExtensionOutput> {
    match args.command {
        ExtensionCommand::List { project } => list(project),
        ExtensionCommand::Show { extension_id } => show_extension(&extension_id),
        ExtensionCommand::Run {
            extension_id,
            project,
            component,
            input,
            step,
            skip,
            args,
            stream,
            no_stream,
        } => run_extension(
            &extension_id,
            project,
            component,
            input,
            args,
            stream,
            no_stream,
            step,
            skip,
        ),
        ExtensionCommand::Setup { extension_id } => setup_extension(&extension_id),
        ExtensionCommand::Install { source, id } => install_extension(&source, id),
        ExtensionCommand::InstallForComponent { source, path } => {
            install_for_component(&source, path.as_deref())
        }
        ExtensionCommand::Update {
            extension_id,
            all,
            force,
        } => update_extension(extension_id.as_deref(), all, force),
        ExtensionCommand::Uninstall { extension_id } => uninstall_extension(&extension_id),
        ExtensionCommand::Action {
            extension_id,
            action_id,
            project,
            data,
        } => run_action(&extension_id, &action_id, project, data),
        ExtensionCommand::Exec {
            extension_id,
            component,
            args,
        } => exec_extension_tool(&extension_id, component, args),
        ExtensionCommand::Set {
            extension_id,
            json,
            replace,
        } => set_extension(extension_id.as_deref(), &json, &replace),
    }
}

#[derive(Serialize)]
#[serde(tag = "command")]
#[allow(clippy::large_enum_variant)]
pub enum ExtensionOutput {
    #[serde(rename = "extension.list")]
    List {
        #[serde(skip_serializing_if = "Option::is_none")]
        project_id: Option<String>,
        extensions: Vec<ExtensionSummary>,
    },
    #[serde(rename = "extension.show")]
    Show { extension: ExtensionDetail },
    #[serde(rename = "extension.run")]
    Run {
        extension_id: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        project_id: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none", flatten)]
        output: Option<homeboy::engine::command::CapturedOutput>,
    },
    #[serde(rename = "extension.setup")]
    Setup { extension_id: String },
    #[serde(rename = "extension.install")]
    Install {
        extension_id: String,
        source: String,
        path: String,
        linked: bool,
        #[serde(skip_serializing_if = "Option::is_none")]
        source_revision: Option<String>,
    },
    #[serde(rename = "extension.install_for_component")]
    InstallForComponent {
        component_id: String,
        source: String,
        installed: Vec<InstallEntry>,
        skipped: Vec<String>,
    },
    #[serde(rename = "extension.update")]
    Update {
        extension_id: String,
        url: String,
        path: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        old_version: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        new_version: Option<String>,
    },
    #[serde(rename = "extension.update_all")]
    UpdateAll {
        updated: Vec<UpdateEntry>,
        skipped: Vec<String>,
    },
    #[serde(rename = "extension.uninstall")]
    Uninstall {
        extension_id: String,
        path: String,
        was_linked: bool,
    },
    #[serde(rename = "extension.action")]
    Action {
        extension_id: String,
        action_id: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        project_id: Option<String>,
        response: serde_json::Value,
    },
    #[serde(rename = "extension.set")]
    Set {
        extension_id: String,
        updated_fields: Vec<String>,
    },
    #[serde(rename = "extension.exec")]
    Exec {
        extension_id: String,
        #[serde(skip_serializing_if = "Option::is_none", flatten)]
        output: Option<homeboy::engine::command::CapturedOutput>,
    },
    #[serde(rename = "extension.set")]
    SetBatch { batch: homeboy::BatchResult },
}

#[derive(Serialize)]
pub struct ExtensionDetail {
    pub id: String,
    pub name: String,
    pub version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub homepage: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_url: Option<String>,
    pub runtime: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runtime_requirements: Option<homeboy::extension::RuntimeRequirementsConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has_setup: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has_ready_check: Option<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,
    pub path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_revision: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cli: Option<CliDetail>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<ActionDetail>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub inputs: Vec<homeboy::extension::InputConfig>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub settings: Vec<homeboy::extension::SettingConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub requires: Option<RequiresDetail>,
}

#[derive(Serialize)]
pub struct InstallEntry {
    pub extension_id: String,
    pub path: String,
    pub linked: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_revision: Option<String>,
}

#[derive(Serialize)]
pub struct CliDetail {
    pub tool: String,
    pub display_name: String,
    pub command_template: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_cli_path: Option<String>,
}

#[derive(Serialize)]
pub struct ActionDetail {
    pub id: String,
    pub label: String,
    #[serde(rename = "type")]
    pub action_type: homeboy::extension::ActionType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub endpoint: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub method: Option<homeboy::extension::HttpMethod>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
}

#[derive(Serialize)]
pub struct RequiresDetail {
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub extensions: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub components: Vec<String>,
}

fn list(project: Option<String>) -> CmdResult<ExtensionOutput> {
    let project_config: Option<Project> = project.as_ref().and_then(|id| project::load(id).ok());
    let summaries = extension::list_summaries(project_config.as_ref());

    Ok((
        ExtensionOutput::List {
            project_id: project,
            extensions: summaries,
        },
        0,
    ))
}

fn show_extension(extension_id: &str) -> CmdResult<ExtensionOutput> {
    let extension = load_extension(extension_id)?;
    let ready_status = extension_ready_status(&extension);
    let linked = is_extension_linked(&extension.id);

    let has_setup = extension
        .runtime()
        .and_then(|r| r.setup_command.as_ref())
        .map(|_| true);
    let has_ready_check = extension
        .runtime()
        .and_then(|r| r.ready_check.as_ref())
        .map(|_| true);

    let cli = extension.cli.as_ref().map(|c| CliDetail {
        tool: c.tool.clone(),
        display_name: c.display_name.clone(),
        command_template: c.command_template.clone(),
        default_cli_path: c.default_cli_path.clone(),
    });

    let actions: Vec<ActionDetail> = extension
        .actions
        .iter()
        .map(|a| ActionDetail {
            id: a.id.clone(),
            label: a.label.clone(),
            action_type: a.action_type.clone(),
            endpoint: a.endpoint.clone(),
            method: a.method.clone(),
            command: a.command.clone(),
        })
        .collect();

    let requires = extension.requires.as_ref().map(|r| RequiresDetail {
        extensions: r.extensions.clone(),
        components: r.components.clone(),
    });

    let source_revision = homeboy::extension::read_source_revision(&extension.id);

    let detail = ExtensionDetail {
        id: extension.id.clone(),
        name: extension.name.clone(),
        version: extension.version.clone(),
        description: extension.description.clone(),
        author: extension.author.clone(),
        homepage: extension.homepage.clone(),
        source_url: extension.source_url.clone(),
        runtime: if extension.executable.is_some() {
            "executable".to_string()
        } else {
            "platform".to_string()
        },
        runtime_requirements: extension.runtime.clone(),
        has_setup,
        has_ready_check,
        ready: ready_status.ready,
        ready_reason: ready_status.reason,
        ready_detail: ready_status.detail,
        linked,
        path: extension.extension_path.clone().unwrap_or_default(),
        source_revision,
        cli,
        actions,
        inputs: extension.inputs().to_vec(),
        settings: extension.settings.clone(),
        requires,
    };

    Ok((ExtensionOutput::Show { extension: detail }, 0))
}

#[allow(clippy::too_many_arguments)]
fn run_extension(
    extension_id: &str,
    project: Option<String>,
    component: Option<String>,
    inputs: Vec<(String, String)>,
    args: Vec<String>,
    stream: bool,
    no_stream: bool,
    step: Option<String>,
    skip: Option<String>,
) -> CmdResult<ExtensionOutput> {
    use homeboy::extension::{ExtensionExecutionMode, ExtensionStepFilter};

    let mode = if no_stream {
        ExtensionExecutionMode::Captured
    } else if stream || crate::commands::utils::tty::is_stdout_tty() {
        ExtensionExecutionMode::Interactive
    } else {
        ExtensionExecutionMode::Captured
    };

    let filter = ExtensionStepFilter { step, skip };

    let result = homeboy::extension::run_extension(
        extension_id,
        project.as_deref(),
        component.as_deref(),
        inputs,
        args,
        mode,
        filter,
    )?;

    Ok((
        ExtensionOutput::Run {
            extension_id: extension_id.to_string(),
            project_id: result.project_id,
            output: result.output,
        },
        result.exit_code,
    ))
}

fn install_extension(source: &str, id: Option<String>) -> CmdResult<ExtensionOutput> {
    let result = homeboy::extension::install(source, id.as_deref())?;
    let linked = is_extension_linked(&result.extension_id);

    Ok((
        ExtensionOutput::Install {
            extension_id: result.extension_id,
            source: result.url,
            path: result.path.to_string_lossy().to_string(),
            linked,
            source_revision: result.source_revision,
        },
        0,
    ))
}

fn install_for_component(source: &str, path: Option<&str>) -> CmdResult<ExtensionOutput> {
    let component = resolve_install_component(path)?;
    let result = homeboy::extension::install_for_component(&component, source)?;

    let installed = result
        .installed
        .into_iter()
        .map(|entry| InstallEntry {
            linked: is_extension_linked(&entry.extension_id),
            extension_id: entry.extension_id,
            path: entry.path.to_string_lossy().to_string(),
            source_revision: entry.source_revision,
        })
        .collect();

    Ok((
        ExtensionOutput::InstallForComponent {
            component_id: result.component_id,
            source: result.source,
            installed,
            skipped: result.skipped,
        },
        0,
    ))
}

fn resolve_install_component(path: Option<&str>) -> homeboy::Result<homeboy::component::Component> {
    if let Some(path) = path {
        return homeboy::component::discover_from_portable(Path::new(path)).ok_or_else(|| {
            homeboy::Error::validation_invalid_argument(
                "path",
                format!("No homeboy.json found at {}", path),
                Some(path.to_string()),
                None,
            )
        });
    }

    homeboy::component::resolve(None)
}

fn update_extension(
    extension_id: Option<&str>,
    all: bool,
    force: bool,
) -> CmdResult<ExtensionOutput> {
    if all {
        return update_all_extensions(force);
    }

    let extension_id = extension_id.ok_or_else(|| {
        homeboy::Error::validation_invalid_argument(
            "extension_id",
            "Provide a extension ID or use --all to update all extensions",
            None,
            None,
        )
    })?;

    // Capture version before update
    let old_version = load_extension(extension_id).ok().map(|m| m.version.clone());

    let result = extension::update(extension_id, force)?;

    // Capture version after update
    let new_version = load_extension(&result.extension_id)
        .ok()
        .map(|m| m.version.clone());

    Ok((
        ExtensionOutput::Update {
            extension_id: result.extension_id,
            url: result.url,
            path: result.path.to_string_lossy().to_string(),
            old_version,
            new_version,
        },
        0,
    ))
}

fn update_all_extensions(force: bool) -> CmdResult<ExtensionOutput> {
    let result = extension::update_all(force);

    Ok((
        ExtensionOutput::UpdateAll {
            updated: result.updated,
            skipped: result.skipped,
        },
        0,
    ))
}

fn uninstall_extension(extension_id: &str) -> CmdResult<ExtensionOutput> {
    let was_linked = is_extension_linked(extension_id);
    let path = homeboy::extension::uninstall(extension_id)?;

    Ok((
        ExtensionOutput::Uninstall {
            extension_id: extension_id.to_string(),
            path: path.to_string_lossy().to_string(),
            was_linked,
        },
        0,
    ))
}

fn setup_extension(extension_id: &str) -> CmdResult<ExtensionOutput> {
    let result = run_setup(extension_id)?;

    Ok((
        ExtensionOutput::Setup {
            extension_id: extension_id.to_string(),
        },
        result.exit_code,
    ))
}

fn run_action(
    extension_id: &str,
    action_id: &str,
    project_id: Option<String>,
    data: Option<String>,
) -> CmdResult<ExtensionOutput> {
    let response = homeboy::extension::run_action(
        extension_id,
        action_id,
        project_id.as_deref(),
        data.as_deref(),
    )?;

    Ok((
        ExtensionOutput::Action {
            extension_id: extension_id.to_string(),
            action_id: action_id.to_string(),
            project_id,
            response,
        },
        0,
    ))
}

fn set_extension(
    extension_id: Option<&str>,
    json: &str,
    replace_fields: &[String],
) -> CmdResult<ExtensionOutput> {
    match homeboy::extension::merge(extension_id, json, replace_fields)? {
        homeboy::MergeOutput::Single(result) => Ok((
            ExtensionOutput::Set {
                extension_id: result.id,
                updated_fields: result.updated_fields,
            },
            0,
        )),
        homeboy::MergeOutput::Bulk(batch) => {
            let exit_code = batch.exit_code();
            Ok((ExtensionOutput::SetBatch { batch }, exit_code))
        }
    }
}

fn exec_extension_tool(
    extension_id: &str,
    component: Option<String>,
    args: Vec<String>,
) -> CmdResult<ExtensionOutput> {
    let exit_code = extension::exec_tool(extension_id, component.as_deref(), &args)?;

    Ok((
        ExtensionOutput::Exec {
            extension_id: extension_id.to_string(),
            output: None,
        },
        exit_code,
    ))
}
