use std::sync::{Arc, LazyLock};

use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, Task, WeakEntity};
use language::{
    LanguageRegistry, LanguageServerName, LspAdapterDelegate,
    language_settings::AllLanguageSettings,
};
use parking_lot::RwLock;
use project::{LspStore, lsp_store::LocalLspAdapterDelegate};
use settings::{LSP_SETTINGS_SCHEMA_URL_PREFIX, Settings as _, SettingsLocation};
use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields};

const SCHEMA_URI_PREFIX: &str = "zed://schemas/";

const TSCONFIG_SCHEMA: &str = include_str!("schemas/tsconfig.json");
const PACKAGE_JSON_SCHEMA: &str = include_str!("schemas/package.json");

static TASKS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
    serde_json::to_string(&task::TaskTemplates::generate_json_schema())
        .expect("TaskTemplates schema should serialize")
});

static SNIPPETS_SCHEMA: LazyLock<String> = LazyLock::new(|| {
    serde_json::to_string(&snippet_provider::format::VsSnippetsFile::generate_json_schema())
        .expect("VsSnippetsFile schema should serialize")
});

static JSONC_SCHEMA: LazyLock<String> = LazyLock::new(|| {
    serde_json::to_string(&generate_jsonc_schema()).expect("JSONC schema should serialize")
});

#[cfg(debug_assertions)]
static INSPECTOR_STYLE_SCHEMA: LazyLock<String> = LazyLock::new(|| {
    serde_json::to_string(&generate_inspector_style_schema())
        .expect("Inspector style schema should serialize")
});

static KEYMAP_SCHEMA: LazyLock<String> = LazyLock::new(|| {
    serde_json::to_string(&settings::KeymapFile::generate_json_schema_from_inventory())
        .expect("Keymap schema should serialize")
});

static ACTION_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
    LazyLock::new(|| RwLock::new(HashMap::default()));

// Runtime cache for dynamic schemas that depend on runtime state:
// - "settings": depends on installed fonts, themes, languages, LSP adapters (extensions can add these)
// - "settings/lsp/*": depends on LSP adapter initialization options
// - "debug_tasks": depends on DAP adapters (extensions can add these)
// Cache is invalidated via notify_schema_changed() when extensions or DAP registry change.
static DYNAMIC_SCHEMA_CACHE: LazyLock<RwLock<HashMap<String, String>>> =
    LazyLock::new(|| RwLock::new(HashMap::default()));

pub fn init(cx: &mut App) {
    cx.set_global(SchemaStore::default());
    project::lsp_store::json_language_server_ext::register_schema_handler(
        handle_schema_request,
        cx,
    );

    cx.observe_new(|_, _, cx| {
        let lsp_store = cx.weak_entity();
        cx.global_mut::<SchemaStore>().lsp_stores.push(lsp_store);
    })
    .detach();

    if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
        cx.subscribe(&extension_events, move |_, evt, cx| match evt {
            extension::Event::ExtensionsInstalledChanged => {
                cx.update_global::<SchemaStore, _>(|schema_store, cx| {
                    schema_store.notify_schema_changed(ChangedSchemas::Settings, cx);
                });
            }
            extension::Event::ExtensionUninstalled(_)
            | extension::Event::ExtensionInstalled(_)
            | extension::Event::ConfigureExtensionRequested(_) => {}
        })
        .detach();
    }

    cx.observe_global::<dap::DapRegistry>(move |cx| {
        cx.update_global::<SchemaStore, _>(|schema_store, cx| {
            schema_store.notify_schema_changed(ChangedSchemas::DebugTasks, cx);
        });
    })
    .detach();
}

#[derive(Default)]
pub struct SchemaStore {
    lsp_stores: Vec<WeakEntity<LspStore>>,
}

impl gpui::Global for SchemaStore {}

enum ChangedSchemas {
    Settings,
    DebugTasks,
}

impl SchemaStore {
    fn notify_schema_changed(&mut self, changed_schemas: ChangedSchemas, cx: &mut App) {
        let uris_to_invalidate = match changed_schemas {
            ChangedSchemas::Settings => {
                let settings_uri_prefix = &format!("{SCHEMA_URI_PREFIX}settings");
                let project_settings_uri = &format!("{SCHEMA_URI_PREFIX}project_settings");
                DYNAMIC_SCHEMA_CACHE
                    .write()
                    .extract_if(|uri, _| {
                        uri == project_settings_uri || uri.starts_with(settings_uri_prefix)
                    })
                    .map(|(url, _)| url)
                    .collect()
            }
            ChangedSchemas::DebugTasks => DYNAMIC_SCHEMA_CACHE
                .write()
                .remove_entry(&format!("{SCHEMA_URI_PREFIX}debug_tasks"))
                .map_or_else(Vec::new, |(uri, _)| vec![uri]),
        };

        if uris_to_invalidate.is_empty() {
            return;
        }

        self.lsp_stores.retain(|lsp_store| {
            let Some(lsp_store) = lsp_store.upgrade() else {
                return false;
            };
            project::lsp_store::json_language_server_ext::notify_schemas_changed(
                lsp_store,
                &uris_to_invalidate,
                cx,
            );
            true
        })
    }
}

pub fn handle_schema_request(
    lsp_store: Entity<LspStore>,
    uri: String,
    cx: &mut AsyncApp,
) -> Task<Result<String>> {
    let path = match uri.strip_prefix(SCHEMA_URI_PREFIX) {
        Some(path) => path,
        None => return Task::ready(Err(anyhow::anyhow!("Invalid schema URI: {}", uri))),
    };

    if let Some(json) = resolve_static_schema(path) {
        return Task::ready(Ok(json));
    }

    if let Some(cached) = DYNAMIC_SCHEMA_CACHE.read().get(&uri).cloned() {
        return Task::ready(Ok(cached));
    }

    let path = path.to_string();
    let uri_clone = uri.clone();
    cx.spawn(async move |cx| {
        let schema = resolve_dynamic_schema(lsp_store, &path, cx).await?;
        let json = serde_json::to_string(&schema).context("Failed to serialize schema")?;

        DYNAMIC_SCHEMA_CACHE.write().insert(uri_clone, json.clone());

        Ok(json)
    })
}

fn resolve_static_schema(path: &str) -> Option<String> {
    let (schema_name, rest) = path.split_once('/').unzip();
    let schema_name = schema_name.unwrap_or(path);

    match schema_name {
        "tsconfig" => Some(TSCONFIG_SCHEMA.to_string()),
        "package_json" => Some(PACKAGE_JSON_SCHEMA.to_string()),
        "tasks" => Some(TASKS_SCHEMA.clone()),
        "snippets" => Some(SNIPPETS_SCHEMA.clone()),
        "jsonc" => Some(JSONC_SCHEMA.clone()),
        "keymap" => Some(KEYMAP_SCHEMA.clone()),
        "zed_inspector_style" => {
            #[cfg(debug_assertions)]
            {
                Some(INSPECTOR_STYLE_SCHEMA.clone())
            }
            #[cfg(not(debug_assertions))]
            {
                Some(
                    serde_json::to_string(&schemars::json_schema!(true).to_value())
                        .expect("true schema should serialize"),
                )
            }
        }

        "action" => {
            let normalized_action_name = match rest {
                Some(name) => name,
                None => return None,
            };
            let action_name = denormalize_action_name(normalized_action_name);

            if let Some(cached) = ACTION_SCHEMA_CACHE.read().get(&action_name).cloned() {
                return Some(cached);
            }

            let mut generator = settings::KeymapFile::action_schema_generator();
            let schema =
                settings::KeymapFile::get_action_schema_by_name(&action_name, &mut generator);
            let json = serde_json::to_string(
                &root_schema_from_action_schema(schema, &mut generator).to_value(),
            )
            .expect("Action schema should serialize");

            ACTION_SCHEMA_CACHE
                .write()
                .insert(action_name, json.clone());
            Some(json)
        }

        _ => None,
    }
}

async fn resolve_dynamic_schema(
    lsp_store: Entity<LspStore>,
    path: &str,
    cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
    let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone());
    let (schema_name, rest) = path.split_once('/').unzip();
    let schema_name = schema_name.unwrap_or(path);

    let schema = match schema_name {
        "settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => {
            let lsp_path = rest
                .and_then(|r| {
                    r.strip_prefix(
                        LSP_SETTINGS_SCHEMA_URL_PREFIX
                            .strip_prefix(SCHEMA_URI_PREFIX)
                            .and_then(|s| s.strip_prefix("settings/"))
                            .unwrap_or("lsp/"),
                    )
                })
                .context("Invalid LSP schema path")?;

            // Parse the schema type from the path:
            // - "rust-analyzer/initialization_options" → initialization_options_schema
            // - "rust-analyzer/settings" → settings_schema
            enum LspSchemaKind {
                InitializationOptions,
                Settings,
            }
            let (lsp_name, schema_kind) = if let Some(adapter_name) =
                lsp_path.strip_suffix("/initialization_options")
            {
                (adapter_name, LspSchemaKind::InitializationOptions)
            } else if let Some(adapter_name) = lsp_path.strip_suffix("/settings") {
                (adapter_name, LspSchemaKind::Settings)
            } else {
                anyhow::bail!(
                    "Invalid LSP schema path: \
                    Expected '{{adapter}}/initialization_options' or '{{adapter}}/settings', got '{}'",
                    lsp_path
                );
            };

            let adapter = languages
                .all_lsp_adapters()
                .into_iter()
                .find(|adapter| adapter.name().as_ref() as &str == lsp_name)
                .or_else(|| {
                    languages.load_available_lsp_adapter(&LanguageServerName::from(lsp_name))
                })
                .with_context(|| format!("LSP adapter not found: {}", lsp_name))?;

            let delegate: Arc<dyn LspAdapterDelegate> = cx
                .update(|inner_cx| {
                    lsp_store.update(inner_cx, |lsp_store, cx| {
                        let Some(local) = lsp_store.as_local() else {
                            return None;
                        };
                        let Some(worktree) = local.worktree_store.read(cx).worktrees().next()
                        else {
                            return None;
                        };
                        Some(LocalLspAdapterDelegate::from_local_lsp(
                            local, &worktree, cx,
                        ))
                    })
                })
                .context(concat!(
                    "Failed to create adapter delegate - ",
                    "either LSP store is not in local mode or no worktree is available"
                ))?;

            let schema = match schema_kind {
                LspSchemaKind::InitializationOptions => {
                    adapter.initialization_options_schema(&delegate, cx).await
                }
                LspSchemaKind::Settings => adapter.settings_schema(&delegate, cx).await,
            };

            schema.unwrap_or_else(|| {
                serde_json::json!({
                    "type": "object",
                    "additionalProperties": true
                })
            })
        }
        "settings" => {
            let mut lsp_adapter_names: Vec<String> = languages
                .all_lsp_adapters()
                .into_iter()
                .map(|adapter| adapter.name())
                .chain(languages.available_lsp_adapter_names())
                .map(|name| name.to_string())
                .collect();

            let mut i = 0;
            while i < lsp_adapter_names.len() {
                let mut j = i + 1;
                while j < lsp_adapter_names.len() {
                    if lsp_adapter_names[i] == lsp_adapter_names[j] {
                        lsp_adapter_names.swap_remove(j);
                    } else {
                        j += 1;
                    }
                }
                i += 1;
            }

            cx.update(|cx| {
                let font_names = &cx.text_system().all_font_names();
                let language_names = &languages
                    .language_names()
                    .into_iter()
                    .map(|name| name.to_string())
                    .collect::<Vec<_>>();

                let mut icon_theme_names = vec![];
                let mut theme_names = vec![];
                if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
                    icon_theme_names.extend(
                        registry
                            .list_icon_themes()
                            .into_iter()
                            .map(|icon_theme| icon_theme.name),
                    );
                    theme_names.extend(registry.list_names());
                }
                let icon_theme_names = icon_theme_names.as_slice();
                let theme_names = theme_names.as_slice();

                let action_names = cx.all_action_names();
                let action_documentation = cx.action_documentation();
                let deprecations = cx.deprecated_actions_to_preferred_actions();
                let deprecation_messages = cx.action_deprecation_messages();

                let mut schema =
                    settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
                        language_names,
                        font_names,
                        theme_names,
                        icon_theme_names,
                        lsp_adapter_names: &lsp_adapter_names,
                        action_names,
                        action_documentation,
                        deprecations,
                        deprecation_messages,
                    });
                inject_feature_flags_schema(&mut schema);
                schema
            })
        }
        "project_settings" => {
            let lsp_adapter_names = languages
                .all_lsp_adapters()
                .into_iter()
                .map(|adapter| adapter.name().to_string())
                .collect::<Vec<_>>();

            let language_names = &languages
                .language_names()
                .into_iter()
                .map(|name| name.to_string())
                .collect::<Vec<_>>();

            let mut schema =
                settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
                    language_names,
                    lsp_adapter_names: &lsp_adapter_names,
                    // These are not allowed in project-specific settings but
                    // they're still fields required by the
                    // `SettingsJsonSchemaParams` struct.
                    font_names: &[],
                    theme_names: &[],
                    icon_theme_names: &[],
                    action_names: &[],
                    action_documentation: &HashMap::default(),
                    deprecations: &HashMap::default(),
                    deprecation_messages: &HashMap::default(),
                });
            inject_feature_flags_schema(&mut schema);
            schema
        }
        "debug_tasks" => {
            let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
                dap_registry.adapters_schema()
            });
            task::DebugTaskFile::generate_json_schema(&adapter_schemas)
        }
        "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions),
        "action" => {
            let normalized_action_name = rest.context("No Action name provided")?;
            let action_name = denormalize_action_name(normalized_action_name);
            let mut generator = settings::KeymapFile::action_schema_generator();
            let schema = cx
                .update(|cx| cx.action_schema_by_name(&action_name, &mut generator))
                .flatten();
            root_schema_from_action_schema(schema, &mut generator).to_value()
        }
        "tasks" => task::TaskTemplates::generate_json_schema(),
        _ => {
            anyhow::bail!("Unrecognized schema: {schema_name}");
        }
    };
    Ok(schema)
}

const JSONC_LANGUAGE_NAME: &str = "JSONC";

pub fn all_schema_file_associations(
    languages: &Arc<LanguageRegistry>,
    path: Option<SettingsLocation<'_>>,
    cx: &mut App,
) -> serde_json::Value {
    let extension_globs = languages
        .available_language_for_name(JSONC_LANGUAGE_NAME)
        .map(|language| language.matcher().path_suffixes.clone())
        .into_iter()
        .flatten()
        // Path suffixes can be entire file names or just their extensions.
        .flat_map(|path_suffix| [format!("*.{path_suffix}"), path_suffix]);
    let override_globs = AllLanguageSettings::get(path, cx)
        .file_types
        .get(JSONC_LANGUAGE_NAME)
        .into_iter()
        .flat_map(|(_, glob_strings)| glob_strings)
        .cloned();
    let jsonc_globs = extension_globs.chain(override_globs).collect::<Vec<_>>();

    let mut file_associations = serde_json::json!([
        {
            "fileMatch": [
                schema_file_match(paths::settings_file()),
            ],
            "url": format!("{SCHEMA_URI_PREFIX}settings"),
        },
        {
            "fileMatch": [
            paths::local_settings_file_relative_path()],
            "url": format!("{SCHEMA_URI_PREFIX}project_settings"),
        },
        {
            "fileMatch": [schema_file_match(paths::keymap_file())],
            "url": format!("{SCHEMA_URI_PREFIX}keymap"),
        },
        {
            "fileMatch": [
                schema_file_match(paths::tasks_file()),
                paths::local_tasks_file_relative_path()
            ],
            "url": format!("{SCHEMA_URI_PREFIX}tasks"),
        },
        {
            "fileMatch": [
                schema_file_match(paths::debug_scenarios_file()),
                paths::local_debug_file_relative_path()
            ],
            "url": format!("{SCHEMA_URI_PREFIX}debug_tasks"),
        },
        {
            "fileMatch": [
                schema_file_match(
                    paths::snippets_dir()
                        .join("*.json")
                        .as_path()
                )
            ],
            "url": format!("{SCHEMA_URI_PREFIX}snippets"),
        },
        {
            "fileMatch": ["tsconfig.json"],
            "url": format!("{SCHEMA_URI_PREFIX}tsconfig")
        },
        {
            "fileMatch": ["package.json"],
            "url": format!("{SCHEMA_URI_PREFIX}package_json")
        },
        {
            "fileMatch": &jsonc_globs,
            "url": format!("{SCHEMA_URI_PREFIX}jsonc")
        },
    ]);

    #[cfg(debug_assertions)]
    {
        file_associations
            .as_array_mut()
            .unwrap()
            .push(serde_json::json!({
                "fileMatch": [
                    "zed-inspector-style.json"
                ],
                "url": format!("{SCHEMA_URI_PREFIX}zed_inspector_style")
            }));
    }

    file_associations
        .as_array_mut()
        .unwrap()
        .extend(cx.all_action_names().into_iter().map(|&name| {
            let normalized_name = normalize_action_name(name);
            let file_name = normalized_action_name_to_file_name(normalized_name.clone());
            serde_json::json!({
                "fileMatch": [file_name],
                "url": format!("{SCHEMA_URI_PREFIX}action/{normalized_name}")
            })
        }));

    file_associations
}

/// Swaps the placeholder [`settings::FeatureFlagsMap`] subschema produced by
/// schemars for an enriched one that lists each known flag's variants. The
/// placeholder is registered in the `settings_content` crate so the
/// `settings` crate doesn't need a reverse dependency on `feature_flags`.
fn inject_feature_flags_schema(schema: &mut serde_json::Value) {
    use schemars::JsonSchema;

    let Some(defs) = schema.get_mut("$defs").and_then(|d| d.as_object_mut()) else {
        return;
    };
    let schema_name = settings::FeatureFlagsMap::schema_name();
    let enriched = feature_flags::generate_feature_flags_schema().to_value();
    defs.insert(schema_name.into_owned(), enriched);
}

fn generate_jsonc_schema() -> serde_json::Value {
    let generator = schemars::generate::SchemaSettings::draft2019_09()
        .with_transform(DefaultDenyUnknownFields)
        .with_transform(AllowTrailingCommas)
        .into_generator();
    let meta_schema = generator
        .settings()
        .meta_schema
        .as_ref()
        .expect("meta_schema should be present in schemars settings")
        .to_string();
    let defs = generator.definitions();
    let schema = schemars::json_schema!({
        "$schema": meta_schema,
        "allowTrailingCommas": true,
        "$defs": defs,
    });
    serde_json::to_value(schema).unwrap()
}

#[cfg(debug_assertions)]
fn generate_inspector_style_schema() -> serde_json::Value {
    let schema = schemars::generate::SchemaSettings::draft2019_09()
        .with_transform(util::schemars::DefaultDenyUnknownFields)
        .into_generator()
        .root_schema_for::<gpui::StyleRefinement>();

    serde_json::to_value(schema).unwrap()
}

pub fn normalize_action_name(action_name: &str) -> String {
    action_name.replace("::", "__")
}

pub fn denormalize_action_name(action_name: &str) -> String {
    action_name.replace("__", "::")
}

pub fn normalized_action_file_name(action_name: &str) -> String {
    normalized_action_name_to_file_name(normalize_action_name(action_name))
}

pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
    normalized_action_name.push_str(".json");
    normalized_action_name
}

fn root_schema_from_action_schema(
    action_schema: Option<schemars::Schema>,
    generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
    let Some(mut action_schema) = action_schema else {
        return schemars::json_schema!(false);
    };
    let meta_schema = generator
        .settings()
        .meta_schema
        .as_ref()
        .expect("meta_schema should be present in schemars settings")
        .to_string();
    let defs = generator.definitions();
    let mut schema = schemars::json_schema!({
        "$schema": meta_schema,
        "allowTrailingCommas": true,
        "$defs": defs,
    });
    schema
        .ensure_object()
        .extend(std::mem::take(action_schema.ensure_object()));
    schema
}

#[inline]
fn schema_file_match(path: &std::path::Path) -> String {
    path.strip_prefix(path.parent().unwrap().parent().unwrap())
        .unwrap()
        .display()
        .to_string()
        .replace('\\', "/")
}
