use super::super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
use super::super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
use super::super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
use super::super::RemotePluginCatalogError;
use super::super::RemotePluginServiceConfig;
use super::local_paths;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_login::CodexAuth;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value as JsonValue;
use serde_json::json;
use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::io::Write;
use std::path::Component;
use std::path::Path;

const PERSONAL_MARKETPLACE_NAME: &str = "codex-curated";
const PERSONAL_MARKETPLACE_DISPLAY_NAME: &str = "Personal";
const PERSONAL_MARKETPLACE_RELATIVE_PATH: &str = ".agents/plugins/marketplace.json";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemotePluginShareCheckoutResult {
    pub remote_plugin_id: String,
    pub plugin_id: String,
    pub plugin_name: String,
    pub plugin_path: AbsolutePathBuf,
    pub marketplace_name: String,
    pub marketplace_path: AbsolutePathBuf,
    pub remote_version: Option<String>,
}

pub async fn checkout_remote_plugin_share(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    codex_home: &Path,
    remote_plugin_id: &str,
) -> Result<RemotePluginShareCheckoutResult, RemotePluginCatalogError> {
    let detail = super::super::fetch_remote_plugin_detail_with_download_urls(
        config,
        auth,
        REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
        remote_plugin_id,
    )
    .await?;
    let plugin_name = detail.summary.name.clone();
    let remote_version = detail.release_version.clone();
    validate_plugin_segment(&plugin_name, "plugin name").map_err(|reason| {
        RemotePluginCatalogError::UnexpectedResponse(format!(
            "remote plugin `{remote_plugin_id}` returned invalid plugin name: {reason}"
        ))
    })?;
    if !is_checkout_supported_share_marketplace(&detail.marketplace_name)
        || detail.summary.share_context.is_none()
    {
        return Err(RemotePluginCatalogError::PluginShareCheckoutNotAvailable {
            remote_plugin_id: remote_plugin_id.to_string(),
        });
    }

    let home = crate::marketplace::home_dir().ok_or_else(|| {
        RemotePluginCatalogError::UnexpectedResponse(
            "could not determine home directory for personal plugin marketplace".to_string(),
        )
    })?;
    let home = AbsolutePathBuf::try_from(home).map_err(|err| {
        RemotePluginCatalogError::UnexpectedResponse(format!(
            "failed to resolve home directory for personal plugin marketplace: {err}"
        ))
    })?;

    let local_paths = load_share_local_paths_for_checkout(codex_home)?;
    let (local_plugin_path, already_checked_out) =
        editable_plugin_path_for_checkout(&home, &plugin_name, remote_plugin_id, &local_paths)?;

    let mut created_checkout_path = false;
    if !already_checked_out {
        let bundle = crate::remote_bundle::validate_remote_plugin_bundle(
            remote_plugin_id,
            &detail.marketplace_name,
            &plugin_name,
            detail.release_version.as_deref(),
            detail.bundle_download_url.as_deref(),
            /*app_manifest*/ None,
        )
        .map_err(|err| {
            RemotePluginCatalogError::UnexpectedResponse(format!(
                "failed to prepare remote plugin bundle checkout: {err}"
            ))
        })?;
        crate::remote_bundle::download_and_extract_remote_plugin_bundle_to_path(
            bundle,
            local_plugin_path.clone(),
        )
        .await
        .map_err(|err| {
            RemotePluginCatalogError::UnexpectedResponse(format!(
                "failed to check out remote plugin bundle: {err}"
            ))
        })?;
        created_checkout_path = true;
    }

    let marketplace = match update_personal_marketplace(
        &home,
        &plugin_name,
        &local_plugin_path,
        detail.summary.install_policy,
        detail.summary.auth_policy,
        detail
            .summary
            .interface
            .as_ref()
            .and_then(|interface| interface.category.clone()),
    ) {
        Ok(marketplace) => marketplace,
        Err(err) => {
            return Err(clean_up_created_checkout_path(
                created_checkout_path,
                &local_plugin_path,
                err,
            ));
        }
    };

    if let Err(err) = local_paths::record_plugin_share_local_path(
        codex_home,
        remote_plugin_id,
        local_plugin_path.clone(),
    ) {
        let err = RemotePluginCatalogError::UnexpectedResponse(format!(
            "failed to record plugin share local path mapping: {err}"
        ));
        return Err(clean_up_created_checkout_path(
            created_checkout_path,
            &local_plugin_path,
            err,
        ));
    }

    let plugin_id = PluginId::new(plugin_name.clone(), marketplace.name.clone())
        .map_err(|err| {
            RemotePluginCatalogError::UnexpectedResponse(format!(
                "failed to build checked out plugin id: {err}"
            ))
        })?
        .as_key();

    Ok(RemotePluginShareCheckoutResult {
        remote_plugin_id: remote_plugin_id.to_string(),
        plugin_id,
        plugin_name,
        plugin_path: local_plugin_path,
        marketplace_name: marketplace.name,
        marketplace_path: marketplace.path,
        remote_version,
    })
}

fn is_checkout_supported_share_marketplace(marketplace_name: &str) -> bool {
    matches!(
        marketplace_name,
        REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME
            | REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME
            | REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME
    )
}

fn load_share_local_paths_for_checkout(
    codex_home: &Path,
) -> Result<BTreeMap<String, AbsolutePathBuf>, RemotePluginCatalogError> {
    match local_paths::load_plugin_share_local_paths(codex_home) {
        Ok(paths) => Ok(paths),
        Err(err) if err.kind() == io::ErrorKind::InvalidData => Ok(BTreeMap::new()),
        Err(err) => Err(RemotePluginCatalogError::UnexpectedResponse(format!(
            "failed to load plugin share local path mapping: {err}"
        ))),
    }
}

fn editable_plugin_path_for_checkout(
    home: &AbsolutePathBuf,
    plugin_name: &str,
    remote_plugin_id: &str,
    local_paths: &BTreeMap<String, AbsolutePathBuf>,
) -> Result<(AbsolutePathBuf, bool), RemotePluginCatalogError> {
    if let Some(existing_path) = local_paths.get(remote_plugin_id)
        && existing_path.as_path().exists()
    {
        ensure_path_can_be_listed_in_personal_marketplace(home, existing_path)?;
        return Ok((existing_path.clone(), true));
    }

    let local_plugin_path = local_paths
        .get(remote_plugin_id)
        .cloned()
        .unwrap_or_else(|| home.join("plugins").join(plugin_name));
    ensure_path_can_be_listed_in_personal_marketplace(home, &local_plugin_path)?;

    if local_plugin_path.as_path().exists() {
        return Err(RemotePluginCatalogError::InvalidPluginPath {
            path: local_plugin_path.to_path_buf(),
            reason: format!(
                "cannot check out remote plugin `{remote_plugin_id}` because the local plugin path already exists"
            ),
        });
    }

    Ok((local_plugin_path, false))
}

fn clean_up_created_checkout_path(
    created_checkout_path: bool,
    local_plugin_path: &AbsolutePathBuf,
    original_err: RemotePluginCatalogError,
) -> RemotePluginCatalogError {
    if !created_checkout_path {
        return original_err;
    }

    match remove_created_checkout_path(local_plugin_path) {
        Ok(()) => original_err,
        Err(cleanup_err) => RemotePluginCatalogError::UnexpectedResponse(format!(
            "{original_err}; additionally failed to clean up checked out plugin path `{}`: {cleanup_err}",
            local_plugin_path.display()
        )),
    }
}

fn remove_created_checkout_path(local_plugin_path: &AbsolutePathBuf) -> io::Result<()> {
    if local_plugin_path.as_path().is_dir() {
        fs::remove_dir_all(local_plugin_path.as_path())
    } else {
        fs::remove_file(local_plugin_path.as_path())
    }
}

fn ensure_path_can_be_listed_in_personal_marketplace(
    home: &AbsolutePathBuf,
    path: &AbsolutePathBuf,
) -> Result<(), RemotePluginCatalogError> {
    personal_marketplace_relative_plugin_path(home, path).map(|_| ())
}

struct PersonalMarketplaceUpdate {
    name: String,
    path: AbsolutePathBuf,
}

fn update_personal_marketplace(
    home: &AbsolutePathBuf,
    plugin_name: &str,
    local_plugin_path: &AbsolutePathBuf,
    install_policy: PluginInstallPolicy,
    auth_policy: PluginAuthPolicy,
    category: Option<String>,
) -> Result<PersonalMarketplaceUpdate, RemotePluginCatalogError> {
    let marketplace_path = home.join(PERSONAL_MARKETPLACE_RELATIVE_PATH);
    let relative_plugin_path = personal_marketplace_relative_plugin_path(home, local_plugin_path)?;
    let mut marketplace = read_or_create_personal_marketplace(marketplace_path.as_path())?;
    let Some(marketplace_object) = marketplace.as_object_mut() else {
        return Err(invalid_marketplace_file(
            marketplace_path.as_path(),
            "personal marketplace file must contain a JSON object",
        ));
    };
    let marketplace_name = marketplace_object
        .entry("name")
        .or_insert_with(|| json!(PERSONAL_MARKETPLACE_NAME))
        .as_str()
        .ok_or_else(|| {
            invalid_marketplace_file(
                marketplace_path.as_path(),
                "marketplace name must be a string",
            )
        })?
        .to_string();
    validate_plugin_segment(&marketplace_name, "marketplace name").map_err(|reason| {
        invalid_marketplace_file(
            marketplace_path.as_path(),
            &format!("marketplace name is invalid: {reason}"),
        )
    })?;

    let plugins = marketplace_object
        .entry("plugins")
        .or_insert_with(|| json!([]))
        .as_array_mut()
        .ok_or_else(|| {
            invalid_marketplace_file(
                marketplace_path.as_path(),
                "marketplace plugins must be an array",
            )
        })?;

    let new_entry = personal_marketplace_plugin_entry(
        plugin_name,
        &relative_plugin_path,
        install_policy,
        auth_policy,
        category,
    );

    if let Some(existing_entry) = plugins
        .iter_mut()
        .find(|entry| entry.get("name").and_then(JsonValue::as_str) == Some(plugin_name))
    {
        let existing_path = existing_entry
            .get("source")
            .and_then(|source| source.get("path"))
            .and_then(JsonValue::as_str);
        if existing_path != Some(relative_plugin_path.as_str()) {
            return Err(invalid_marketplace_file(
                marketplace_path.as_path(),
                &format!(
                    "marketplace already contains plugin `{plugin_name}` with a different source path"
                ),
            ));
        }
        *existing_entry = new_entry;
    } else {
        plugins.push(new_entry);
    }

    let contents = serde_json::to_string_pretty(&marketplace)
        .map_err(|err| RemotePluginCatalogError::UnexpectedResponse(err.to_string()))?;
    write_json_atomically(marketplace_path.as_path(), &format!("{contents}\n")).map_err(|err| {
        RemotePluginCatalogError::UnexpectedResponse(format!(
            "failed to update personal plugin marketplace: {err}"
        ))
    })?;

    Ok(PersonalMarketplaceUpdate {
        name: marketplace_name,
        path: marketplace_path,
    })
}

fn read_or_create_personal_marketplace(
    marketplace_path: &Path,
) -> Result<JsonValue, RemotePluginCatalogError> {
    match std::fs::read_to_string(marketplace_path) {
        Ok(contents) => serde_json::from_str(&contents).map_err(|err| {
            invalid_marketplace_file(
                marketplace_path,
                &format!("failed to parse personal marketplace file: {err}"),
            )
        }),
        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(json!({
            "name": PERSONAL_MARKETPLACE_NAME,
            "interface": {
                "displayName": PERSONAL_MARKETPLACE_DISPLAY_NAME,
            },
            "plugins": [],
        })),
        Err(err) => Err(RemotePluginCatalogError::UnexpectedResponse(format!(
            "failed to read personal plugin marketplace: {err}"
        ))),
    }
}

fn personal_marketplace_plugin_entry(
    plugin_name: &str,
    relative_plugin_path: &str,
    install_policy: PluginInstallPolicy,
    auth_policy: PluginAuthPolicy,
    category: Option<String>,
) -> JsonValue {
    let mut entry = json!({
        "name": plugin_name,
        "source": {
            "source": "local",
            "path": relative_plugin_path,
        },
        "policy": {
            "installation": plugin_install_policy_value(install_policy),
            "authentication": plugin_auth_policy_value(auth_policy),
        },
    });
    if let Some(category) = category
        && !category.trim().is_empty()
        && let Some(object) = entry.as_object_mut()
    {
        object.insert("category".to_string(), json!(category));
    }
    entry
}

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

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

fn personal_marketplace_relative_plugin_path(
    home: &AbsolutePathBuf,
    local_plugin_path: &AbsolutePathBuf,
) -> Result<String, RemotePluginCatalogError> {
    let relative = local_plugin_path
        .as_path()
        .strip_prefix(home.as_path())
        .map_err(|_| RemotePluginCatalogError::InvalidPluginPath {
            path: local_plugin_path.to_path_buf(),
            reason: "local plugin path must be inside the home directory to be listed in the personal marketplace".to_string(),
        })?;
    let mut segments = Vec::new();
    for component in relative.components() {
        match component {
            Component::Normal(segment) => {
                let segment = segment.to_str().ok_or_else(|| {
                    RemotePluginCatalogError::InvalidPluginPath {
                        path: local_plugin_path.to_path_buf(),
                        reason: "local plugin path contains non-UTF-8 segments".to_string(),
                    }
                })?;
                segments.push(segment.to_string());
            }
            Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
                return Err(RemotePluginCatalogError::InvalidPluginPath {
                    path: local_plugin_path.to_path_buf(),
                    reason:
                        "local plugin path cannot be represented as a personal marketplace path"
                            .to_string(),
                });
            }
        }
    }
    if segments.is_empty() {
        return Err(RemotePluginCatalogError::InvalidPluginPath {
            path: local_plugin_path.to_path_buf(),
            reason: "local plugin path must not be the home directory".to_string(),
        });
    }
    Ok(format!("./{}", segments.join("/")))
}

fn invalid_marketplace_file(path: &Path, message: &str) -> RemotePluginCatalogError {
    RemotePluginCatalogError::InvalidPluginPath {
        path: path.to_path_buf(),
        reason: message.to_string(),
    }
}

fn write_json_atomically(write_path: &Path, contents: &str) -> io::Result<()> {
    let parent = write_path.parent().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("path {} has no parent directory", write_path.display()),
        )
    })?;
    std::fs::create_dir_all(parent)?;
    let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
    tmp.write_all(contents.as_bytes())?;
    tmp.persist(write_path).map_err(|err| err.error)?;
    Ok(())
}
