use std::{borrow::Cow, ops::Deref, str::FromStr, sync::Arc};

use crate::resolve_json_pointer;
use crate::{
    AllOfSchema, AnyOfSchema, CatalogUri, DocumentSchema, OneOfSchema, PatternAccessor,
    PatternAccessors, SourceSchema, SubSchemaUriMap, ValueSchema, get_tombi_schemastore_content,
    http_client::HttpClient, json::JsonCatalog,
};
use itertools::{Either, Itertools};
use tokio::sync::RwLock;
use tombi_ast::SchemaDocumentCommentDirective;
use tombi_cache::{get_cache_file_path, read_from_cache, refresh_cache, save_to_cache};
use tombi_config::{SchemaItem, SchemaOverviewOptions, TomlVersion, config_base_dir};
use tombi_future::{BoxFuture, Boxable};
use tombi_uri::SchemaUri;

type DocumentSchemas = Arc<RwLock<tombi_hashmap::HashMap<SchemaUri, CachedDocumentSchema>>>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SchemaCacheVersion {
    modified_at_nanos: u64,
    len: u64,
}

#[derive(Debug, Clone)]
struct CachedDocumentSchema {
    version: Option<SchemaCacheVersion>,
    document_schema: Result<Arc<DocumentSchema>, crate::Error>,
}

/// Options for associating a schema with file patterns
#[derive(Debug, Clone, Default)]
pub struct AssociateSchemaOptions {
    pub title: Option<String>,
    pub description: Option<String>,
    pub toml_version: Option<TomlVersion>,
    /// If true, the schema will be inserted at the beginning to force precedence
    pub force: bool,
}

#[derive(Debug, Clone)]
pub struct SchemaStore {
    http_client: HttpClient,
    document_schemas: DocumentSchemas,
    schemas: Arc<RwLock<Vec<crate::Schema>>>,
    options: crate::Options,
    base_dir_path: Arc<RwLock<Option<std::path::PathBuf>>>,
}

impl Default for SchemaStore {
    fn default() -> Self {
        Self::new()
    }
}

impl SchemaStore {
    /// New with default options
    ///
    /// Create an empty store.
    /// Note that the new() does not automatically load schemas from Config etc.
    pub fn new() -> Self {
        Self::new_with_options(crate::Options::default())
    }

    pub async fn is_empty(&self) -> bool {
        self.document_schemas.read().await.is_empty() && self.schemas.read().await.is_empty()
    }

    /// New with options
    ///
    /// Create a store with the given options.
    /// Note that the new_with_options() does not automatically load schemas from Config etc.
    pub fn new_with_options(options: crate::Options) -> Self {
        Self {
            http_client: HttpClient::new(),
            document_schemas: Arc::new(RwLock::default()),
            schemas: Arc::new(RwLock::new(Vec::new())),
            options,
            base_dir_path: Arc::new(RwLock::new(None)),
        }
    }

    /// Offline mode
    pub fn offline(&self) -> bool {
        self.options.offline.unwrap_or_default()
    }

    /// Cache options
    pub fn cache_options(&self) -> Option<&tombi_cache::Options> {
        self.options.cache.as_ref()
    }

    /// Strict mode
    pub fn strict(&self) -> bool {
        self.options.strict.unwrap_or(true)
    }

    pub async fn refresh_cache(
        &self,
        config: &tombi_config::Config,
        config_path: Option<&std::path::Path>,
    ) -> Result<bool, crate::Error> {
        if refresh_cache().await? {
            self.reload_config(config, config_path).await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    pub async fn reload_config(
        &self,
        config: &tombi_config::Config,
        config_path: Option<&std::path::Path>,
    ) -> Result<(), crate::Error> {
        self.document_schemas.write().await.clear();
        self.schemas.write().await.clear();
        self.load_config(config, config_path).await?;
        Ok(())
    }

    pub async fn load_config(
        &self,
        config: &tombi_config::Config,
        config_path: Option<&std::path::Path>,
    ) -> Result<(), crate::Error> {
        let base_dir_path_buf = config_path
            .and_then(config_base_dir)
            .map(canonicalize_path_for_matching);
        let base_dir_path = base_dir_path_buf.as_deref();

        // Set the base directory for schema matching
        *self.base_dir_path.write().await = base_dir_path_buf.clone();

        let schema_options = match &config.schema {
            Some(schema) => schema,
            None => &SchemaOverviewOptions::default(),
        };

        if schema_options.enabled.unwrap_or_default().value() {
            self.load_config_schemas(
                match &config.schemas {
                    Some(schemas) => schemas,
                    None => &[],
                },
                base_dir_path,
            )
            .await;

            let catalog_paths = schema_options.catalog_paths().unwrap_or_default();

            let catalogs_results =
                futures::future::join_all(catalog_paths.iter().map(|catalog_path| async move {
                    let Ok(catalog_uri) = catalog_path
                        .try_to_catalog_url(base_dir_path)
                        .map(CatalogUri::from)
                    else {
                        return Err(crate::Error::CatalogPathConvertUriFailed {
                            catalog_path: catalog_path.to_string(),
                        });
                    };
                    let catalog_uri = Arc::new(catalog_uri);
                    self.load_catalog_from_uri(&catalog_uri)
                        .await
                        .map(|catalog| catalog.map(|catalog| (catalog_uri.clone(), catalog)))
                }))
                .await;

            for catalog_result in catalogs_results {
                match catalog_result {
                    Ok(Some((catalog_uri, catalog))) => {
                        self.add_json_catalog(catalog_uri, catalog).await?;
                    }
                    Ok(None) => {}
                    Err(e) => return Err(e),
                }
            }
        }

        Ok(())
    }

    fn normalize_schema_uri_key(schema_uri: &SchemaUri) -> SchemaUri {
        let mut normalized = schema_uri.clone();
        normalized.set_fragment(None);
        normalized
    }

    async fn load_config_schemas(
        &self,
        schemas: &[SchemaItem],
        base_dir_path: Option<&std::path::Path>,
    ) {
        futures::future::join_all(schemas.iter().map(|schema| async move {
            let schema_uri = if let Ok(schema_uri) = SchemaUri::from_str(schema.path()) {
                schema_uri
            } else if let Ok(schema_uri) = match base_dir_path {
                Some(base_dir_path) => SchemaUri::from_file_path(base_dir_path.join(schema.path())),
                None => SchemaUri::from_file_path(schema.path()),
            } {
                schema_uri
            } else {
                log::warn!("Invalid schema path: {}", schema.path());
                return;
            };

            log::debug!("Load schema from config: {}", schema_uri);

            self.schemas.write().await.push(crate::Schema {
                title: None,
                description: None,
                deprecated_lint_level: schema.deprecated_lint_level(),
                format_rules: schema.format().and_then(|format| format.rules.clone()),
                lint_rules: schema.lint().and_then(|lint| lint.rules.clone()),
                overrides: schema_overrides(schema),
                schema_uri,
                catalog_uri: None,
                include: schema.include().to_vec(),
                exclude: schema.exclude().map(|exclude| exclude.to_vec()),
                toml_version: schema.toml_version(),
                sub_root_accessors: schema.root().and_then(PatternAccessor::parse),
            });
        }))
        .await;
    }

    pub async fn load_catalog_from_uri(
        &self,
        catalog_uri: &CatalogUri,
    ) -> Result<Option<JsonCatalog>, crate::Error> {
        Ok(Some(match catalog_uri.scheme() {
            "file" => {
                let catalog_path = catalog_uri.to_file_path().map_err(|_| {
                    crate::Error::InvalidCatalogFileUri {
                        catalog_uri: catalog_uri.clone(),
                    }
                })?;

                if !catalog_path.exists() {
                    return Err(crate::Error::CatalogFileNotFound {
                        catalog_path: catalog_path.to_path_buf(),
                    });
                }

                let content = std::fs::read_to_string(&catalog_path).map_err(|_| {
                    crate::Error::CatalogFileReadFailed {
                        catalog_path: catalog_path.to_path_buf(),
                    }
                })?;

                log::debug!("load catalog from file: {}", catalog_uri);

                serde_json::from_str(&content).map_err(|err| crate::Error::InvalidJsonFormat {
                    uri: catalog_uri.deref().clone(),
                    reason: err.to_string(),
                })?
            }
            "http" | "https" => {
                let catalog_cache_path = get_cache_file_path(catalog_uri).await;
                if let Some(catalog_cache_path) = &catalog_cache_path
                    && let Ok(Some(catalog)) = load_catalog_from_cache(
                        catalog_uri,
                        catalog_cache_path,
                        self.options.cache.as_ref(),
                    )
                    .await
                {
                    return Ok(Some(catalog));
                }

                if self.offline() {
                    if let Ok(Some(catalog)) = load_catalog_from_cache_ignoring_ttl(
                        catalog_uri,
                        catalog_cache_path.as_deref(),
                        self.options.cache.clone(),
                    )
                    .await
                    {
                        return Ok(Some(catalog));
                    }
                    log::debug!("offline mode, skip fetch catalog from url: {}", catalog_uri);
                    return Ok(None);
                }

                let bytes = match self.http_client.get_bytes(catalog_uri.as_str()).await {
                    Ok(bytes) => {
                        log::debug!("fetch catalog from url: {}", catalog_uri);
                        bytes
                    }
                    Err(err) => {
                        if let Ok(Some(catalog)) = load_catalog_from_cache_ignoring_ttl(
                            catalog_uri,
                            catalog_cache_path.as_deref(),
                            self.options.cache.clone(),
                        )
                        .await
                        {
                            return Ok(Some(catalog));
                        }
                        return Err(crate::Error::CatalogUriFetchFailed {
                            catalog_uri: catalog_uri.clone(),
                            reason: err.to_string(),
                        });
                    }
                };

                if let Err(err) = save_to_cache(catalog_cache_path.as_deref(), &bytes).await {
                    log::warn!("{err}");
                }

                match serde_json::from_slice::<crate::json::JsonCatalog>(&bytes) {
                    Ok(catalog) => catalog,
                    Err(err) => {
                        return Err(crate::Error::InvalidJsonFormat {
                            uri: catalog_uri.deref().clone(),
                            reason: err.to_string(),
                        });
                    }
                }
            }
            "tombi" => {
                let Some(content) = get_tombi_schemastore_content(catalog_uri) else {
                    return Err(crate::Error::InvalidCatalogFileUri {
                        catalog_uri: catalog_uri.clone(),
                    });
                };

                serde_json::from_str::<crate::json::JsonCatalog>(content).map_err(|err| {
                    crate::Error::InvalidJsonFormat {
                        uri: catalog_uri.deref().clone(),
                        reason: err.to_string(),
                    }
                })?
            }
            _ => {
                return Err(crate::Error::UnsupportedUriScheme {
                    uri: catalog_uri.deref().clone(),
                });
            }
        }))
    }

    async fn add_json_catalog(
        &self,
        catalog_uri: Arc<CatalogUri>,
        json_catalog: JsonCatalog,
    ) -> Result<(), crate::Error> {
        let mut schemas = self.schemas.write().await;
        for schema in json_catalog.schemas {
            if schema
                .file_match
                .iter()
                .any(|pattern| pattern.ends_with(".toml"))
            {
                schemas.push(crate::Schema {
                    title: Some(schema.name),
                    description: Some(schema.description),
                    deprecated_lint_level: None,
                    format_rules: None,
                    lint_rules: None,
                    overrides: Default::default(),
                    schema_uri: schema.url,
                    catalog_uri: Some(catalog_uri.clone()),
                    include: schema.file_match,
                    exclude: None,
                    toml_version: None,
                    sub_root_accessors: None,
                });
            }
        }
        Ok(())
    }

    pub async fn update_schema(&self, mut schema_uri: SchemaUri) -> Result<bool, crate::Error> {
        if matches!(schema_uri.scheme(), "http" | "https") && self.offline() {
            log::debug!("offline mode, skip fetch schema from url: {}", schema_uri);
            return Ok(false);
        }

        if schema_uri.fragment().is_some() {
            schema_uri.set_fragment(None);
        }

        let has_key = { self.document_schemas.read().await.contains_key(&schema_uri) };
        if has_key
            && let Some(document_schema) = self.fetch_document_schema(&schema_uri).await.transpose()
        {
            let version = schema_cache_version(&schema_uri).await;
            self.document_schemas.write().await.insert(
                schema_uri.clone(),
                CachedDocumentSchema {
                    version,
                    document_schema,
                },
            );
            log::debug!("update schema: {}", schema_uri);
            return Ok(true);
        }

        Ok(false)
    }

    pub async fn fetch_schema_value(
        &self,
        schema_uri: &SchemaUri,
    ) -> Result<Option<tombi_json::ValueNode>, crate::Error> {
        match schema_uri.scheme() {
            "file" => {
                let schema_path = tombi_uri::Uri::to_file_path(schema_uri).map_err(|_| {
                    crate::Error::InvalidSchemaUri {
                        schema_uri: schema_uri.to_string(),
                    }
                })?;

                if !schema_path.exists() {
                    return Err(crate::Error::SchemaFileNotFound {
                        schema_path: schema_path.clone(),
                    });
                }

                let file = std::fs::File::open(&schema_path)
                    .map_err(|_| crate::Error::SchemaFileReadFailed { schema_path })?;

                log::debug!("fetch schema from file: {}", schema_uri);

                Ok(Some(tombi_json::ValueNode::from_reader(file).map_err(
                    |err| crate::Error::SchemaFileParseFailed {
                        schema_uri: schema_uri.to_owned(),
                        reason: err.to_string(),
                    },
                )?))
            }
            "http" | "https" => {
                let schema_cache_path = get_cache_file_path(schema_uri).await;
                if let Some(schema_cache_path) = &schema_cache_path
                    && let Ok(Some(schema_value)) = load_json_schema_from_cache(
                        schema_uri,
                        schema_cache_path,
                        self.options.cache.as_ref(),
                    )
                    .await
                {
                    return Ok(Some(schema_value));
                }

                if self.offline() {
                    if let Ok(Some(schema_value)) = load_json_schema_from_cache_ignoring_ttl(
                        schema_uri,
                        schema_cache_path.as_deref(),
                        self.options.cache.clone(),
                    )
                    .await
                    {
                        return Ok(Some(schema_value));
                    }
                    log::debug!("offline mode, skip fetch schema from uri: {}", schema_uri);
                    return Ok(None);
                }

                let bytes = match self.http_client.get_bytes(schema_uri.as_str()).await {
                    Ok(bytes) => {
                        log::debug!("fetch schema from uri: {}", schema_uri);
                        bytes
                    }
                    Err(err) => {
                        if let Ok(Some(schema_value)) = load_json_schema_from_cache_ignoring_ttl(
                            schema_uri,
                            schema_cache_path.as_deref(),
                            self.options.cache.clone(),
                        )
                        .await
                        {
                            return Ok(Some(schema_value));
                        }
                        return Err(crate::Error::SchemaFetchFailed {
                            schema_uri: schema_uri.clone(),
                            reason: err.to_string(),
                        });
                    }
                };

                if let Err(err) = save_to_cache(schema_cache_path.as_deref(), &bytes).await {
                    log::warn!("{err}");
                }

                Ok(Some(
                    tombi_json::ValueNode::from_reader(std::io::Cursor::new(bytes)).map_err(
                        |err| crate::Error::SchemaFileParseFailed {
                            schema_uri: schema_uri.to_owned(),
                            reason: err.to_string(),
                        },
                    )?,
                ))
            }
            "tombi" => {
                let Some(content) = get_tombi_schemastore_content(schema_uri) else {
                    return Err(crate::Error::SchemaResourceNotFound {
                        schema_uri: schema_uri.to_owned(),
                    });
                };

                log::debug!("fetch schema from embedded file: {}", schema_uri);

                Ok(Some(tombi_json::ValueNode::from_str(content).map_err(
                    |err| crate::Error::SchemaFileParseFailed {
                        schema_uri: schema_uri.to_owned(),
                        reason: err.to_string(),
                    },
                )?))
            }
            _ => Err(crate::Error::UnsupportedUriScheme {
                uri: schema_uri.deref().clone(),
            }),
        }
    }

    async fn fetch_document_schema(
        &self,
        schema_uri: &SchemaUri,
    ) -> Result<Option<Arc<DocumentSchema>>, crate::Error> {
        let schema_value = match self.fetch_schema_value(schema_uri).await? {
            Some(value) => value,
            None => return Ok(None),
        };
        if !matches!(
            schema_value,
            tombi_json::ValueNode::Object(_) | tombi_json::ValueNode::Bool(_)
        ) {
            return Err(crate::Error::SchemaMustBeObjectOrBoolean {
                schema_uri: schema_uri.clone(),
            });
        }
        let document_schema = DocumentSchema::new(schema_value, schema_uri.clone(), self).await;
        if let Some(
            ValueSchema::AllOf(AllOfSchema { schemas, .. })
            | ValueSchema::AnyOf(AnyOfSchema { schemas, .. })
            | ValueSchema::OneOf(OneOfSchema { schemas, .. }),
        ) = document_schema.value_schema.as_deref()
        {
            let document_base_uri = document_schema.base_uri().clone();
            {
                for referable_schema in schemas.write().await.iter_mut() {
                    referable_schema
                        .resolve(
                            Cow::Borrowed(&document_base_uri),
                            Cow::Borrowed(&document_schema.definitions),
                            self,
                        )
                        .await?;
                }
            }
        }

        Ok(Some(Arc::new(document_schema)))
    }

    pub fn try_get_document_schema<'a: 'b, 'b>(
        &'a self,
        schema_uri: &'a SchemaUri,
    ) -> BoxFuture<'b, Result<Option<Arc<DocumentSchema>>, crate::Error>> {
        async move {
            let requested_schema_uri = schema_uri.clone();

            let (schema_uri, fragment) = {
                let mut uri = schema_uri.clone();
                let fragment = uri.fragment().map(ToOwned::to_owned);
                uri.set_fragment(None);
                (uri, fragment)
            };

            let cached_document_schema =
                self.document_schemas.read().await.get(&schema_uri).cloned();
            let document_schema = if let Some(cached_document_schema) = cached_document_schema {
                let cache_version = schema_cache_version(&schema_uri).await;
                if cached_document_schema.version == cache_version {
                    match cached_document_schema.document_schema {
                        Ok(document_schema) => Some(document_schema),
                        Err(err) => return Err(err),
                    }
                } else {
                    match self.fetch_document_schema(&schema_uri).await.transpose() {
                        Some(document_schema) => {
                            let cache_version = schema_cache_version(&schema_uri).await;
                            self.document_schemas.write().await.insert(
                                schema_uri.clone(),
                                CachedDocumentSchema {
                                    version: cache_version,
                                    document_schema: document_schema.clone(),
                                },
                            );
                            Some(document_schema?)
                        }
                        None => None,
                    }
                }
            } else {
                match self.fetch_document_schema(&schema_uri).await.transpose() {
                    Some(document_schema) => {
                        let cache_version = schema_cache_version(&schema_uri).await;
                        self.document_schemas.write().await.insert(
                            schema_uri.clone(),
                            CachedDocumentSchema {
                                version: cache_version,
                                document_schema: document_schema.clone(),
                            },
                        );
                        Some(document_schema?)
                    }
                    None => None,
                }
            };

            let Some(document_schema) = document_schema else {
                return Ok(None);
            };

            // If no fragment, return the base document schema as-is
            let Some(fragment) = fragment else {
                return Ok(Some(document_schema));
            };

            let fragment_reference = format!("#{fragment}");

            // Handle JSON Pointer fragments (e.g., "#/definitions/TableValue")
            if fragment_reference == "#" || fragment_reference.starts_with("#/") {
                let Some(schema_value) = self.fetch_schema_value(&schema_uri).await? else {
                    return Ok(None);
                };

                let Some(fragment_value_schema) = resolve_json_pointer(
                    &schema_value,
                    &fragment_reference,
                    document_schema.string_formats(),
                    document_schema.dialect(),
                )?
                else {
                    return Err(crate::Error::InvalidJsonPointer {
                        pointer: fragment_reference,
                        schema_uri,
                    });
                };

                // Create a new document schema with the fragment-referenced schema_uri in the return value
                let mut fragment_document_schema = document_schema.as_ref().clone();
                fragment_document_schema.schema_uri = requested_schema_uri; // Use fragment-full URI for return value
                fragment_document_schema.value_schema = Some(Arc::new(fragment_value_schema));
                return Ok(Some(Arc::new(fragment_document_schema)));
            }

            // Handle anchor fragments (e.g., "#anchorName")
            let anchor_schema = {
                let anchors = document_schema.anchors.read().await;
                if let Some(schema) = anchors.get(&fragment_reference).cloned() {
                    Some(schema)
                } else {
                    drop(anchors);
                    let dynamic_anchors = document_schema.dynamic_anchors.read().await;
                    dynamic_anchors.get(&fragment_reference).cloned()
                }
            };

            if let Some(anchor_schema) = anchor_schema
                && let Some(current_schema) = anchor_schema
                    .to_current_schema(
                        Cow::Borrowed(document_schema.base_uri()),
                        Cow::Borrowed(&document_schema.definitions),
                        self,
                    )
                    .await?
            {
                // Create a new document schema with the fragment-referenced schema_uri in the return value
                let mut fragment_document_schema = document_schema.as_ref().clone();
                fragment_document_schema.schema_uri = requested_schema_uri; // Use fragment-full URI for return value
                fragment_document_schema.value_schema = Some(current_schema.value_schema);
                return Ok(Some(Arc::new(fragment_document_schema)));
            }

            Err(crate::Error::InvalidJsonSchemaReference {
                reference: fragment_reference,
                schema_uri,
            })
        }
        .boxed()
    }

    #[inline]
    async fn try_get_source_schema_from_remote_url(
        &self,
        schema_uri: &SchemaUri,
        source_path: Option<&std::path::Path>,
    ) -> Result<Option<SourceSchema>, crate::Error> {
        let source_schema = if let Some(source_path) = source_path {
            self.resolve_source_schema_from_path(source_path)
                .await
                .ok()
                .flatten()
        } else {
            None
        };

        let (
            root_schema,
            sub_schema_uri_map,
            toml_version,
            deprecated_lint_level,
            schema_format_rules,
            schema_lint_rules,
            schema_overrides,
        ) = if let Some(source_schema) = source_schema {
            let toml_version = source_schema.toml_version();
            (
                source_schema.root_schema,
                source_schema.sub_schema_uri_map,
                toml_version,
                source_schema.deprecated_lint_level,
                source_schema.schema_format_rules,
                source_schema.schema_lint_rules,
                source_schema.schema_overrides,
            )
        } else {
            (
                None,
                Default::default(),
                None,
                None,
                Default::default(),
                Default::default(),
                Default::default(),
            )
        };

        Ok(Some(SourceSchema::new(
            self.try_get_document_schema(schema_uri)
                .await?
                .or(root_schema),
            sub_schema_uri_map,
            toml_version,
            deprecated_lint_level,
            schema_format_rules,
            schema_lint_rules,
            schema_overrides,
        )))
    }

    pub async fn resolve_source_schema_from_ast(
        &self,
        root: &tombi_ast::Root,
        source_uri_or_path: Option<Either<&tombi_uri::Uri, &std::path::Path>>,
    ) -> Result<Option<SourceSchema>, (crate::Error, tombi_text::Range)> {
        let source_path = match source_uri_or_path {
            Some(Either::Left(url)) => match url.scheme() {
                "file" => tombi_uri::Uri::to_file_path(url).ok(),
                _ => None,
            },
            Some(Either::Right(path)) => {
                Some(std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()))
            }
            None => None,
        };

        if let Some(SchemaDocumentCommentDirective { uri, uri_range, .. }) =
            root.schema_document_comment_directive(source_path.as_deref())
        {
            let schema_uri = match uri {
                Ok(schema_uri) => schema_uri,
                Err(schema_uri_or_file_path) => {
                    return Err((
                        crate::Error::InvalidSchemaUriOrFilePath {
                            schema_uri_or_file_path,
                        },
                        uri_range,
                    ));
                }
            };
            return self
                .try_get_source_schema_from_remote_url(&schema_uri, source_path.as_deref())
                .await
                .map_err(|err| (err, uri_range));
        }

        if let Some(source_uri_or_path) = source_uri_or_path {
            Ok(self
                .resolve_source_schema(source_uri_or_path)
                .await
                .ok()
                .flatten())
        } else {
            Ok(None)
        }
    }

    async fn resolve_source_schema_from_path(
        &self,
        source_path: &std::path::Path,
    ) -> Result<Option<SourceSchema>, crate::Error> {
        let canonicalized_source_path = canonicalize_path_for_matching(source_path);

        // Get the base directory for relative path conversion
        let base_dir_path = self.base_dir_path.read().await;

        // Determine the path to use for pattern matching without per-call filesystem I/O
        let path_for_matching = base_dir_path
            .as_deref()
            .and_then(|base_dir_path| {
                canonicalized_source_path
                    .strip_prefix(base_dir_path)
                    .ok()
                    .map(|relative_source_path| relative_source_path.to_path_buf())
            })
            .unwrap_or_else(|| canonicalized_source_path.clone());

        let schemas = self.schemas.read().await;
        let matching_schemas = schemas
            .iter()
            .filter(|schema| {
                matches_schema_patterns(
                    &schema.include,
                    schema.exclude.as_deref(),
                    &path_for_matching,
                    &canonicalized_source_path,
                )
            })
            .collect_vec();

        let mut source_schema: Option<SourceSchema> = None;
        for matching_schema in matching_schemas {
            // Skip if the same schema (by URL and sub_root_accessors) is already loaded in source_schema
            let already_loaded = match &matching_schema.sub_root_accessors {
                Some(sub_root_accessors) => source_schema.as_ref().is_some_and(|source_schema| {
                    source_schema
                        .sub_schema_uri_map
                        .contains_key(sub_root_accessors)
                }),
                None => source_schema
                    .as_ref()
                    .is_some_and(|source_schema| source_schema.root_schema.is_some()),
            };
            if already_loaded {
                continue;
            }
            match self
                .try_get_document_schema(&matching_schema.schema_uri)
                .await
            {
                Ok(Some(document_schema)) => match &matching_schema.sub_root_accessors {
                    Some(sub_root_accessors) => match source_schema {
                        Some(ref mut source_schema) => {
                            if !source_schema
                                .sub_schema_uri_map
                                .contains_key(sub_root_accessors)
                            {
                                let schema_uri_key =
                                    Self::normalize_schema_uri_key(&document_schema.schema_uri);
                                source_schema.sub_schema_uri_map.insert(
                                    sub_root_accessors.clone(),
                                    document_schema.schema_uri.clone(),
                                );
                                if let Some(format_rules) = &matching_schema.format_rules {
                                    source_schema
                                        .schema_format_rules
                                        .insert(schema_uri_key.clone(), format_rules.clone());
                                }
                                if let Some(lint_rules) = &matching_schema.lint_rules {
                                    source_schema
                                        .schema_lint_rules
                                        .insert(schema_uri_key.clone(), lint_rules.clone());
                                }
                                source_schema
                                    .schema_overrides
                                    .insert(schema_uri_key, matching_schema.overrides.clone());
                            }
                        }
                        None => {
                            let schema_uri_key =
                                Self::normalize_schema_uri_key(&document_schema.schema_uri);
                            let mut sub_schema_uri_map = SubSchemaUriMap::default();
                            sub_schema_uri_map.insert(
                                sub_root_accessors.clone(),
                                document_schema.schema_uri.clone(),
                            );
                            let mut schema_format_rules = crate::SchemaFormatRulesMap::default();
                            if let Some(format_rules) = &matching_schema.format_rules {
                                schema_format_rules
                                    .insert(schema_uri_key.clone(), format_rules.clone());
                            }
                            let mut schema_lint_rules = crate::SchemaLintRulesMap::default();
                            if let Some(lint_rules) = &matching_schema.lint_rules {
                                schema_lint_rules
                                    .insert(schema_uri_key.clone(), lint_rules.clone());
                            }
                            let mut schema_overrides = crate::SchemaOverridesMap::default();
                            schema_overrides
                                .insert(schema_uri_key, matching_schema.overrides.clone());
                            let new_source = SourceSchema::new(
                                None,
                                sub_schema_uri_map,
                                matching_schema.toml_version,
                                matching_schema.deprecated_lint_level,
                                schema_format_rules,
                                schema_lint_rules,
                                schema_overrides,
                            );
                            source_schema = Some(new_source);
                        }
                    },
                    None => match source_schema {
                        Some(ref mut existing) => {
                            if existing.root_schema.is_none() {
                                let schema_uri_key =
                                    Self::normalize_schema_uri_key(&document_schema.schema_uri);
                                let toml_version =
                                    existing.toml_version().or(matching_schema.toml_version);
                                let sub_schema_uri_map =
                                    std::mem::take(&mut existing.sub_schema_uri_map);
                                let mut schema_format_rules =
                                    std::mem::take(&mut existing.schema_format_rules);
                                let mut schema_lint_rules =
                                    std::mem::take(&mut existing.schema_lint_rules);
                                let mut schema_overrides =
                                    std::mem::take(&mut existing.schema_overrides);
                                if let Some(format_rules) = &matching_schema.format_rules {
                                    schema_format_rules
                                        .insert(schema_uri_key.clone(), format_rules.clone());
                                }
                                if let Some(lint_rules) = &matching_schema.lint_rules {
                                    schema_lint_rules
                                        .insert(schema_uri_key.clone(), lint_rules.clone());
                                }
                                schema_overrides
                                    .insert(schema_uri_key, matching_schema.overrides.clone());
                                *existing = SourceSchema::new(
                                    Some(document_schema),
                                    sub_schema_uri_map,
                                    toml_version,
                                    matching_schema.deprecated_lint_level,
                                    schema_format_rules,
                                    schema_lint_rules,
                                    schema_overrides,
                                );
                            }
                        }
                        None => {
                            let schema_uri_key =
                                Self::normalize_schema_uri_key(&document_schema.schema_uri);
                            let mut schema_format_rules = crate::SchemaFormatRulesMap::default();
                            if let Some(format_rules) = &matching_schema.format_rules {
                                schema_format_rules
                                    .insert(schema_uri_key.clone(), format_rules.clone());
                            }
                            let mut schema_lint_rules = crate::SchemaLintRulesMap::default();
                            if let Some(lint_rules) = &matching_schema.lint_rules {
                                schema_lint_rules
                                    .insert(schema_uri_key.clone(), lint_rules.clone());
                            }
                            let mut schema_overrides = crate::SchemaOverridesMap::default();
                            schema_overrides
                                .insert(schema_uri_key, matching_schema.overrides.clone());
                            let new_source = SourceSchema::new(
                                Some(document_schema),
                                Default::default(),
                                matching_schema.toml_version,
                                matching_schema.deprecated_lint_level,
                                schema_format_rules,
                                schema_lint_rules,
                                schema_overrides,
                            );
                            source_schema = Some(new_source);
                        }
                    },
                },
                Ok(None) => {
                    log::warn!(
                        "Failed to find document schema: {}",
                        matching_schema.schema_uri
                    );
                }
                Err(err) => {
                    log::warn!(
                        "Failed to get document schema for {url}: {err}",
                        url = matching_schema.schema_uri,
                    );
                }
            }
        }

        Ok(source_schema)
    }

    async fn resolve_source_schema_from_uri(
        &self,
        source_uri: &tombi_uri::Uri,
    ) -> Result<Option<SourceSchema>, crate::Error> {
        match source_uri.scheme() {
            "file" => {
                let source_path = tombi_uri::Uri::to_file_path(source_uri).map_err(|_| {
                    crate::Error::SourceUriParseFailed {
                        source_uri: source_uri.to_owned(),
                    }
                })?;
                self.resolve_source_schema_from_path(&source_path).await
            }
            "untitled" => Ok(None),
            _ => Err(crate::Error::UnsupportedSourceUri {
                source_uri: source_uri.to_owned(),
            }),
        }
    }

    pub(crate) async fn resolve_source_schema(
        &self,
        source_uri_or_path: Either<&tombi_uri::Uri, &std::path::Path>,
    ) -> Result<Option<SourceSchema>, crate::Error> {
        match source_uri_or_path {
            Either::Left(source_uri) => self.resolve_source_schema_from_uri(source_uri).await,
            Either::Right(source_path) => self.resolve_source_schema_from_path(source_path).await,
        }
        .inspect(|source_schema| {
            if let Some(source_schema) = source_schema {
                if let Some(root_schema) = &source_schema.root_schema {
                    log::trace!("find root schema from {}", root_schema.schema_uri);
                }
                for (accessors, schema_uri) in &source_schema.sub_schema_uri_map {
                    log::trace!(
                        "find sub schema {:?} from {}",
                        PatternAccessors::from(accessors.clone()),
                        schema_uri
                    );
                }
            }
        })
    }

    pub async fn associate_schema(
        &self,
        schema_uri: SchemaUri,
        include: Vec<String>,
        options: &AssociateSchemaOptions,
    ) {
        let include = include
            .into_iter()
            .map(|pattern| canonicalize_file_match_pattern(&pattern))
            .collect();

        let new_schema = crate::Schema {
            title: options.title.clone(),
            description: options.description.clone(),
            deprecated_lint_level: None,
            format_rules: None,
            lint_rules: None,
            overrides: Default::default(),
            schema_uri,
            catalog_uri: None,
            include,
            exclude: None,
            toml_version: options.toml_version,
            sub_root_accessors: None,
        };

        let mut schemas = self.schemas.write().await;
        if options.force {
            // Insert at the beginning to force precedence
            schemas.insert(0, new_schema);
        } else {
            // Append at the end
            schemas.push(new_schema);
        }
    }

    pub async fn list_schemas(&self) -> Vec<crate::Schema> {
        self.schemas.read().await.clone()
    }
}

async fn schema_cache_version(schema_uri: &SchemaUri) -> Option<SchemaCacheVersion> {
    let path = match schema_uri.scheme() {
        "file" => tombi_uri::Uri::to_file_path(schema_uri).ok(),
        "http" | "https" => get_cache_file_path(schema_uri).await,
        _ => None,
    }?;

    let metadata = tokio::fs::metadata(path).await.ok()?;
    let modified = metadata.modified().ok()?;
    let duration = modified.duration_since(std::time::UNIX_EPOCH).ok()?;

    Some(SchemaCacheVersion {
        modified_at_nanos: duration
            .as_secs()
            .saturating_mul(1_000_000_000)
            .saturating_add(u64::from(duration.subsec_nanos())),
        len: metadata.len(),
    })
}

fn matches_schema_patterns(
    include: &[String],
    exclude: Option<&[String]>,
    path_for_matching: &std::path::Path,
    absolute_source_path: &std::path::Path,
) -> bool {
    let included = include.iter().any(|pat| {
        let pattern = glob_pattern_for_file_match(pat);

        glob::Pattern::new(&pattern)
            .ok()
            .map(|glob_pat| {
                glob_pat.matches_path(path_for_matching)
                    || (path_for_matching != absolute_source_path
                        && glob_pat.matches_path(absolute_source_path))
            })
            .unwrap_or_default()
    });

    included
        && !exclude.is_some_and(|exclude| {
            exclude.iter().any(|pat| {
                let pattern = glob_pattern_for_file_match(pat);

                glob::Pattern::new(&pattern)
                    .ok()
                    .map(|glob_pat| {
                        glob_pat.matches_path(path_for_matching)
                            || (path_for_matching != absolute_source_path
                                && glob_pat.matches_path(absolute_source_path))
                    })
                    .unwrap_or_default()
            })
        })
}

fn glob_pattern_for_file_match(pattern: &str) -> String {
    if pattern.contains('*') || std::path::Path::new(pattern).is_absolute() {
        pattern.to_string()
    } else {
        format!("**/{pattern}")
    }
}

fn canonicalize_file_match_pattern(pattern: &str) -> String {
    let path = std::path::Path::new(pattern);
    if pattern.contains('*') || !path.is_absolute() {
        pattern.to_string()
    } else {
        canonicalize_path_for_matching(path)
            .to_string_lossy()
            .into_owned()
    }
}

async fn load_catalog_from_cache_ignoring_ttl(
    tagalog_uri: &CatalogUri,
    catalog_cache_path: Option<&std::path::Path>,
    cache_options: Option<tombi_cache::Options>,
) -> Result<Option<JsonCatalog>, crate::Error> {
    if let Some(catalog_cache_path) = catalog_cache_path {
        let mut owned_cache_options = cache_options.unwrap_or_default();
        owned_cache_options.cache_ttl = None;
        if let Ok(Some(catalog)) =
            load_catalog_from_cache(tagalog_uri, catalog_cache_path, Some(&owned_cache_options))
                .await
        {
            return Ok(Some(catalog));
        }
    }

    Ok(None)
}

async fn load_catalog_from_cache(
    tagalog_uri: &CatalogUri,
    catalog_cache_path: &std::path::Path,
    cache_options: Option<&tombi_cache::Options>,
) -> Result<Option<JsonCatalog>, crate::Error> {
    if let Some(catalog_cache_content) =
        read_from_cache(Some(catalog_cache_path), cache_options).await?
    {
        log::debug!("load catalog from cache: {}", tagalog_uri);

        return Ok(Some(serde_json::from_str(&catalog_cache_content).map_err(
            |err| crate::Error::CatalogFileParseFailed {
                tagalog_uri: tagalog_uri.to_owned(),
                reason: err.to_string(),
            },
        )?));
    }

    Ok(None)
}

/// Attempt to load the json schema from the cache, ignoring the TTL.
async fn load_json_schema_from_cache_ignoring_ttl(
    schema_uri: &SchemaUri,
    schema_cache_path: Option<&std::path::Path>,
    cache_options: Option<tombi_cache::Options>,
) -> Result<Option<tombi_json::ValueNode>, crate::Error> {
    if let Some(schema_cache_path) = schema_cache_path {
        let mut owned_cache_options = cache_options.unwrap_or_default();
        owned_cache_options.cache_ttl = None;
        if let Ok(Some(schema_value)) =
            load_json_schema_from_cache(schema_uri, schema_cache_path, Some(&owned_cache_options))
                .await
        {
            return Ok(Some(schema_value));
        }
    }

    Ok(None)
}

async fn load_json_schema_from_cache(
    schema_uri: &SchemaUri,
    schema_cache_path: &std::path::Path,
    cache_options: Option<&tombi_cache::Options>,
) -> Result<Option<tombi_json::ValueNode>, crate::Error> {
    if let Some(schema_cache_content) =
        read_from_cache(Some(schema_cache_path), cache_options).await?
    {
        log::debug!("load schema from cache: {}", schema_uri);

        return Ok(Some(
            tombi_json::ValueNode::from_str(&schema_cache_content).map_err(|err| {
                crate::Error::SchemaFileParseFailed {
                    schema_uri: schema_uri.to_owned(),
                    reason: err.to_string(),
                }
            })?,
        ));
    }

    Ok(None)
}

fn canonicalize_path_for_matching(path: &std::path::Path) -> std::path::PathBuf {
    std::fs::canonicalize(path).unwrap_or_else(|_| {
        if path.is_absolute() {
            path.to_path_buf()
        } else {
            std::env::current_dir()
                .ok()
                .map(|current_dir| current_dir.join(path))
                .unwrap_or_else(|| path.to_path_buf())
        }
    })
}

fn schema_overrides(schema: &tombi_config::SchemaItem) -> crate::SchemaOverrides {
    let mut overrides = crate::SchemaOverrides::default();

    for override_item in schema.overrides().into_iter().flatten() {
        let format_rules = override_item
            .format
            .as_ref()
            .and_then(|format| format.rules.as_ref());
        let lint_rules = override_item
            .lint
            .as_ref()
            .and_then(|lint| lint.rules.as_ref());
        let targets = override_item
            .targets
            .iter()
            .filter_map(|target| parse_override_target(target))
            .collect_vec();

        if let Some(level) = lint_rules.and_then(|r| r.deprecated) {
            overrides.deprecated.extend(
                targets
                    .iter()
                    .cloned()
                    .map(|target| crate::DeprecatedOverride { target, level }),
            );
        }

        if let Some(rule) = format_rules.and_then(|r| r.array_values_order.as_ref()) {
            let disabled = !rule.enabled().unwrap_or_default().value();
            let order = rule.order();
            overrides
                .array_values_order
                .extend(
                    targets
                        .iter()
                        .cloned()
                        .map(|target| crate::ArrayOrderOverride {
                            target,
                            disabled,
                            order,
                        }),
                );
        }

        if let Some(rule) = format_rules.and_then(|r| r.table_keys_order.as_ref()) {
            let disabled = !rule.enabled().unwrap_or_default().value();
            let order = rule.order();
            overrides
                .table_keys_order
                .extend(
                    targets
                        .iter()
                        .cloned()
                        .map(|target| crate::TableOrderOverride {
                            target,
                            disabled,
                            order,
                        }),
                );
        }
    }

    overrides
}

fn parse_override_target(target: &str) -> Option<Vec<PatternAccessor>> {
    if target.is_empty() {
        Some(Vec::new())
    } else {
        PatternAccessor::parse(target)
    }
}

#[cfg(test)]
mod tests {
    use std::{
        fs,
        path::{Path, PathBuf},
        str::FromStr,
        time::Duration,
    };

    use super::{
        SchemaStore, load_catalog_from_cache_ignoring_ttl,
        load_json_schema_from_cache_ignoring_ttl, matches_schema_patterns,
    };
    use crate::{CatalogUri, ValueSchema};
    use tombi_uri::SchemaUri;

    fn temp_cache_path(test_name: &str) -> PathBuf {
        let unique = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        std::env::temp_dir().join(format!("tombi-schema-store-{test_name}-{unique}.json"))
    }

    fn bump_modified(path: &Path) {
        let file = fs::File::options().write(true).open(path).unwrap();
        let modified = file.metadata().unwrap().modified().unwrap() + Duration::from_secs(1);
        file.set_modified(modified).unwrap();
    }

    #[test]
    fn schema_include_matches_user_config_path_via_absolute_suffix() {
        assert!(matches_schema_patterns(
            &[String::from("tombi/config.toml")],
            None,
            Path::new("config.toml"),
            Path::new("/Users/test/.config/tombi/config.toml"),
        ));
    }

    #[test]
    fn schema_include_does_not_match_unrelated_config_toml() {
        assert!(!matches_schema_patterns(
            &[String::from("tombi/config.toml")],
            None,
            Path::new("config.toml"),
            Path::new("/Users/test/project/config.toml"),
        ));
    }

    #[test]
    fn schema_exclude_blocks_matching_path() {
        assert!(!matches_schema_patterns(
            &[String::from("**/*.toml")],
            Some(&[String::from("vendor/**/*.toml")]),
            Path::new("vendor/blocked.toml"),
            Path::new("/Users/test/project/vendor/blocked.toml"),
        ));
    }

    #[test]
    fn schema_include_matches_absolute_posix_path() {
        let absolute_path = Path::new("/Users/test/project/selected-schema.toml");
        let include = [absolute_path.to_string_lossy().into_owned()];

        assert!(matches_schema_patterns(
            &include,
            None,
            absolute_path,
            absolute_path,
        ));
    }

    #[cfg(windows)]
    #[test]
    fn schema_include_matches_absolute_windows_path() {
        let absolute_path = Path::new(r"C:\Users\test\project\selected-schema.toml");
        let include = [absolute_path.to_string_lossy().into_owned()];

        assert!(matches_schema_patterns(
            &include,
            None,
            absolute_path,
            absolute_path,
        ));
    }

    #[tokio::test]
    async fn fragment_pointer_resolves_boolean_schema() {
        let schema_path = std::env::temp_dir().join(format!(
            "tombi_fragment_boolean_{}_{}.json",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::write(
            &schema_path,
            r#"{
                "$defs": {
                    "allowAll": true
                }
            }"#,
        )
        .unwrap();

        let schema_uri = SchemaUri::from_str(&format!(
            "{}#/$defs/allowAll",
            SchemaUri::from_file_path(&schema_path).unwrap()
        ))
        .unwrap();
        let schema_store = SchemaStore::new();

        let document_schema = schema_store
            .try_get_document_schema(&schema_uri)
            .await
            .unwrap()
            .unwrap();

        std::assert_matches!(
            document_schema.value_schema.as_deref(),
            Some(ValueSchema::Anything(_))
        );

        let _ = std::fs::remove_file(schema_path);
    }

    #[tokio::test]
    async fn fragment_anchor_resolves_dynamic_anchor() {
        let schema_path = std::env::temp_dir().join(format!(
            "tombi_fragment_dynamic_anchor_{}_{}.json",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::write(
            &schema_path,
            r#"{
                "$schema": "https://json-schema.org/draft/2020-12/schema",
                "$defs": {
                    "name": {
                        "$dynamicAnchor": "nameSchema",
                        "type": "string"
                    }
                }
            }"#,
        )
        .unwrap();

        let schema_uri = SchemaUri::from_str(&format!(
            "{}#nameSchema",
            SchemaUri::from_file_path(&schema_path).unwrap()
        ))
        .unwrap();
        let schema_store = SchemaStore::new();

        let document_schema = schema_store
            .try_get_document_schema(&schema_uri)
            .await
            .unwrap()
            .unwrap();

        std::assert_matches!(
            document_schema.value_schema.as_deref(),
            Some(ValueSchema::String(_))
        );

        let _ = std::fs::remove_file(schema_path);
    }

    #[tokio::test]
    async fn reloads_cached_file_schema_when_file_changes() {
        let schema_path = std::env::temp_dir().join(format!(
            "tombi_reload_schema_{}_{}.json",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        let schema_uri = SchemaUri::from_file_path(&schema_path).unwrap();
        let schema_store = SchemaStore::new();

        std::fs::write(&schema_path, r#"{"type":"string"}"#).unwrap();

        let document_schema = schema_store
            .try_get_document_schema(&schema_uri)
            .await
            .unwrap()
            .unwrap();

        std::assert_matches!(
            document_schema.value_schema.as_deref(),
            Some(ValueSchema::String(_))
        );

        std::fs::write(&schema_path, r#"{"type":"integer"}"#).unwrap();
        bump_modified(&schema_path);

        let document_schema = schema_store
            .try_get_document_schema(&schema_uri)
            .await
            .unwrap()
            .unwrap();

        std::assert_matches!(
            document_schema.value_schema.as_deref(),
            Some(ValueSchema::Integer(_))
        );

        let _ = std::fs::remove_file(schema_path);
    }

    #[tokio::test]
    async fn ignores_ttl_for_catalog_cache_without_cache_options() {
        let cache_path = temp_cache_path("catalog-cache-offline-default-options");
        std::fs::write(
            &cache_path,
            r#"{"schemas":[{"name":"test","description":"desc","url":"https://example.invalid/schema.json"}]}"#,
        )
        .unwrap();
        std::fs::File::options()
            .write(true)
            .open(&cache_path)
            .unwrap()
            .set_modified(std::time::SystemTime::now() - Duration::from_secs(60 * 60 * 25))
            .unwrap();

        let catalog = load_catalog_from_cache_ignoring_ttl(
            &CatalogUri::from_str("https://example.invalid/catalog.json").unwrap(),
            Some(&cache_path),
            None,
        )
        .await
        .unwrap();

        assert_eq!(catalog.unwrap().schemas.len(), 1);

        let _ = std::fs::remove_file(cache_path);
    }

    #[tokio::test]
    async fn ignores_ttl_for_schema_cache_without_cache_options() {
        let cache_path = temp_cache_path("schema-cache-offline-default-options");
        std::fs::write(&cache_path, r#"{"type":"string"}"#).unwrap();
        std::fs::File::options()
            .write(true)
            .open(&cache_path)
            .unwrap()
            .set_modified(std::time::SystemTime::now() - Duration::from_secs(60 * 60 * 25))
            .unwrap();

        let schema = load_json_schema_from_cache_ignoring_ttl(
            &SchemaUri::from_str("https://example.invalid/schema.json").unwrap(),
            Some(&cache_path),
            None,
        )
        .await
        .unwrap();

        assert!(schema.is_some());

        let _ = std::fs::remove_file(cache_path);
    }
}
