from __future__ import annotations

import os
from abc import ABC, abstractmethod
from collections.abc import Mapping
from pathlib import Path
from typing import (
    Any,
    Final,
    Literal,
    TypeVar,
)

from pydantic import BaseModel, ValidationError
from pydantic.json_schema import GenerateJsonSchema

from fmu.datamodels.types import VersionStr

T = TypeVar("T", dict, list, object)


class FmuSchemas:
    """These URLs can be constructed programmatically from radixconfig.yaml if need be:

        {cfg.components[].name}-{cfg.metadata.name}-{spec.environments[].name}

    As they are unlikely to change they are hardcoded here.
    """

    DEV_URL: Final[str] = "https://main-fmu-schemas-dev.radix.equinor.com"
    PROD_URL: Final[str] = "https://main-fmu-schemas-prod.radix.equinor.com"
    PATH: Final[Path] = Path("schemas")


class GenerateJsonSchemaBase(GenerateJsonSchema):
    """Implements a schema generator so that some additional fields may be
    added.

    This class also collects static methods used to transform the default OpenAPI
    schemas generated by Pydantic into schemas compatible with JSON Schema specs."""

    @staticmethod
    def remove_discriminator_mapping(data: T) -> T:
        """
        Removes entries with key ["discriminator"]["mapping"] from the schema. This
        adjustment is necessary because JSON Schema does not recognize this value
        while OpenAPI does.
        """

        if isinstance(data, dict):
            if "discriminator" in data and isinstance(data["discriminator"], dict):
                data["discriminator"].pop("mapping", None)

            for key, value in data.items():
                data[key] = GenerateJsonSchemaBase.remove_discriminator_mapping(value)

        elif isinstance(data, list):
            for index, element in enumerate(data):
                data[index] = GenerateJsonSchemaBase.remove_discriminator_mapping(
                    element
                )

        return data

    @staticmethod
    def remove_format_path(data: T) -> T:
        """
        Removes entries with key ["format"] = "path" from the schema. This
        adjustment is necessary because JSON Schema does not recognize the "format":
        "path", while OpenAPI does. This function is used in contexts where OpenAPI
        specifications are not applicable.
        """

        if isinstance(data, dict):
            return {
                k: GenerateJsonSchemaBase.remove_format_path(v)
                for k, v in data.items()
                if not (k == "format" and v == "path")
            }

        if isinstance(data, list):
            return [
                GenerateJsonSchemaBase.remove_format_path(element) for element in data
            ]

        return data

    def generate(
        self,
        schema: Mapping[str, Any],
        mode: Literal["validation", "serialization"] = "validation",
    ) -> dict[str, Any]:
        json_schema = super().generate(schema, mode=mode)

        json_schema = GenerateJsonSchemaBase.remove_discriminator_mapping(json_schema)
        json_schema = GenerateJsonSchemaBase.remove_format_path(json_schema)
        json_schema["$schema"] = self.schema_dialect

        return json_schema


class SchemaBase(ABC):
    VERSION: VersionStr
    """The current version of the schema."""

    VERSION_CHANGELOG: str
    """The changelog for all versions of schemas."""

    FILENAME: str
    """The filename, i.e. schema.json."""

    PATH: Path
    """The on-disk _and_ URL path following the domain, i.e:

        schemas/0.1.0/schema.json

    This path should _always_ have `FmuSchemas.PATH` as its first parent.
    This determines the on-disk and URL location of this schema file. A
    trivial example is:

        PATH: Path = FmuSchemas.PATH / VERSION / FILENAME

    """

    @classmethod
    def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None:
        """This achieves Pydantic-like validation without being Pydantic.

        The reason for not using Pydantic here is that it does not play well with
        the class methods that are derived, and the dump method that must be
        implemented. It also doesn't like the default generator.
        """
        super().__init_subclass__(**kwargs)
        cls._validate_class_vars_set()
        cls._validate_version()
        cls._validate_version_changelog()
        cls._validate_path()

    @classmethod
    def _validate_class_vars_set(cls) -> None:
        for attr in ("VERSION", "VERSION_CHANGELOG", "FILENAME", "PATH"):
            if not hasattr(cls, attr):
                raise TypeError(f"Subclass {cls.__name__} must define '{attr}'")

    @classmethod
    def _validate_path(cls) -> None:
        if not cls.PATH.parts[0].startswith(str(FmuSchemas.PATH)):
            raise ValueError(
                f"PATH must start with `FmuSchemas.PATH`: {FmuSchemas.PATH}. "
                f"Got {cls.PATH}"
            )

    @classmethod
    def _validate_version(cls) -> None:
        try:

            class PydanticVersionValidator(BaseModel):
                """VersionStr uses a Pydantic 'Field'. Use Pydantic to validate it."""

                version: VersionStr

            PydanticVersionValidator(version=cls.VERSION)
        except ValidationError as e:
            raise TypeError(f"Invalid VERSION format for '{cls.__name__}': {e}") from e

    @classmethod
    def _validate_version_changelog(cls) -> None:
        """Ensures that VERSION has a doc string that looks like a changelog.

        Checks that the current version has an entry as well."""
        if f"### {cls.VERSION}" not in cls.VERSION_CHANGELOG:
            raise ValueError(
                f"No changelog entry exists for '{cls.__name__}' version {cls.VERSION}."
                f" Expected a header like '### {cls.VERSION}' in 'VERSION_CHANGELOG' "
                f"with changes for version {cls.VERSION} listed beneath it."
            )

    @classmethod
    def dev_url(cls) -> str:
        """Returns the url to the schema on the Radix dev environment."""
        return f"{FmuSchemas.DEV_URL}/{cls.PATH}"

    @classmethod
    def prod_url(cls) -> str:
        """Returns the url to the schema on the Radix prod environment."""
        return f"{FmuSchemas.PROD_URL}/{cls.PATH}"

    @classmethod
    def url(cls) -> str:
        """Returns the URL the schema resides at.

        Schema URLs are composed from class variables set by children derived from this
        base class. If we're in a development environment, or Komodo bleeding, return
        the dev schema URL."""
        if os.environ.get("DEV_SCHEMA", None) or "bleeding" in os.environ.get(
            "KOMODO_RELEASE", os.environ.get("KOMODO_RELEASE_BACKUP", "")
        ):
            return cls.dev_url()
        return cls.prod_url()

    @classmethod
    def default_generator(cls) -> type[GenerateJsonSchema]:
        """Provides a default schema generator that should be adequate for most simple
        schemas.

        When more customization is required a separate schema generator may be
        warranted. See the 'FmuResults' model for how this can be done."""

        class DefaultGenerateJsonSchema(GenerateJsonSchemaBase):
            """Implements a schema generator so that some additional fields may be
            added."""

            def generate(
                self,
                schema: Mapping[str, Any],
                mode: Literal["validation", "serialization"] = "validation",
            ) -> dict[str, Any]:
                json_schema = super().generate(schema, mode=mode)

                json_schema["$id"] = cls.url()
                json_schema["version"] = cls.VERSION

                return json_schema

        return DefaultGenerateJsonSchema

    @classmethod
    @abstractmethod
    def dump(cls) -> dict[str, Any]:
        """
        Dumps the export root model to JSON format for schema validation and
        usage in FMU data structures.

        To update the schema:
            1. Run the following CLI command to dump the updated schema:
                `./tools/update-schemas --diff`.
            2. Check the diff for changes. Adding fields usually indicates non-breaking
                changes and is generally safe. However, if fields are removed, it could
                indicate breaking changes that may affect dependent systems. Perform a
                quality control (QC) check to ensure these changes do not break existing
                implementations.
                If changes are satisfactory and do not introduce issues, commit
                them to maintain schema consistency.
        """
        raise NotImplementedError
