from __future__ import annotations

from enum import StrEnum
from types import SimpleNamespace
from typing import Annotated, Any, List

import httpx
from httpx import Client as httpxClient
from pydantic import (
    BaseModel,
    BeforeValidator,
    Field,
    PlainSerializer,
    WithJsonSchema,
    computed_field,
    model_validator,
)

from fbnconfig import datatype

from .resource_abc import CamelAlias, FetchMixin, ListMixin, Ref, Resource, register_resource

ResourceId = datatype.ResourceId
DataTypeKey = datatype.DataTypeKey

GROUP = "Data Model"


class FieldCategory(StrEnum):
    SERIES_IDENTIFIER = "SeriesIdentifier"
    VALUE = "Value"
    METADATA = "Metadata"


class ApplicableEntityType(StrEnum):
    INSTRUMENT = "Instrument"
    LEGAL_ENTITY = "LegalEntity"
    PERSON = "Person"
    INVESTOR_RECORD = "InvestorRecord"
    INVESTMENT_ACCOUNT = "InvestmentAccount"
    PORTFOLIO = "Portfolio"
    TRANSACTION = "Transaction"
    HOLDING = "Holding"
    SETTLEMENT_INSTRUCTION = "SettlementInstruction"


class FieldSchema(BaseModel, CamelAlias):
    """Define a field in the relational dataset schema"""
    field_name: str
    display_name: str | None = None
    description: str | None = None
    data_type_id: DataTypeKey
    required: bool = True
    category:  FieldCategory


@register_resource(group=GROUP)
class RelationalDatasetDefinitionRef(BaseModel, Ref):
    """Reference an existing relational dataset definition

    Example
    -------
    >>> from fbnconfig import relational_dataset_definition
    >>> relational_dataset_definition.RelationalDatasetDefinitionRef(
    ...     id="sample-dataset-ref",
    ...     scope="default",
    ...     code="sample-dataset"
    ... )

    Attributes
    ----------
    id : str
        Resource identifier.
    scope : str
        Scope of the dataset definition.
    code : str
        Code of the dataset definition.
    """

    id: str = Field(exclude=True)
    scope: str
    code: str

    def attach(self, client):
        try:
            client.get(f"/api/api/relationaldatasetdefinitions/{self.scope}/{self.code}")
        except httpx.HTTPStatusError as ex:
            if ex.response.status_code == 404:
                raise RuntimeError(
                    f"Relational dataset definition {self.scope}/{self.code} does not exist"
                )
            else:
                raise ex


@register_resource(group=GROUP, keys=["scope", "code"])
class RelationalDatasetDefinitionResource(BaseModel, Resource, ListMixin, FetchMixin, CamelAlias):
    """Define a relational dataset definition

    Example
    -------
    >>> from fbnconfig import relational_dataset_definition, datatype
    >>> relational_dataset_definition.RelationalDatasetDefinitionResource(
    ...     id="sample-dataset",
    ...     scope="default",
    ...     code="sample-dataset",
    ...     display_name="Sample Financial Dataset",
    ...     description="A sample relational dataset definition",
    ...     applicable_entity_types=["Instrument", "Portfolio"],
    ...     field_schema=[
    ...         FieldSchema(
    ...             field_name="instrumentId",
    ...             display_name="Instrument ID",
    ...             description="The unique identifier",
    ...             data_type_id=datatype.ResourceId(scope="system", code="string"),
    ...             required=True,
    ...             category="SeriesIdentifier"
    ...         )
    ...     ]
    ... )

    Attributes
    ----------
    id : str
        Resource identifier.
    scope : str
        Scope of the dataset definition.
    code : str
        Code of the dataset definition.
    display_name : str
        Human-readable name for the dataset.
    description : str | None
        Optional description of the dataset.
    applicable_entity_types : List[str]
        List of entity types this dataset can be applied to.
    field_schema : List[FieldSchema]
        The schema defining the fields in this dataset.
    """

    id: str = Field(exclude=True)
    scope: str
    code: str
    display_name: str
    description: str | None = None
    applicable_entity_types: List[ApplicableEntityType]
    field_schema: List[FieldSchema]

    @model_validator(mode="before")
    @classmethod
    def des_model(cls, data: Any, info) -> dict[str, Any]:
        if not isinstance(data, dict):
            return data
        if info.context and info.context.get("id"):
            return data | {"id": info.context.get("id")}
        return data

    @computed_field(alias="id")
    def dataset_id(self) -> dict[str, str]:
        return {"scope": self.scope, "code": self.code}

    def read(self, client: httpxClient, old_state: SimpleNamespace):
        scope = old_state.scope
        code = old_state.code
        return client.get(f"/api/api/relationaldatasetdefinitions/{scope}/{code}").json()

    def create(self, client: httpxClient):
        desired = self.model_dump(
            mode="json",
            exclude_none=True,
            by_alias=True,
            exclude={"scope", "code"}
        )
        client.post("/api/api/relationaldatasetdefinitions", json=desired)
        return {
            "scope": self.scope,
            "code": self.code,
        }

    def update(self, client: httpxClient, old_state: SimpleNamespace) -> dict[str, Any] | None:
        if [self.scope, self.code] != [old_state.scope, old_state.code]:
            self.delete(client, old_state)
            return self.create(client)

        remote = self.read(client, old_state)

        # Remove fields that shouldn't be compared
        remote.pop("version", None)
        remote.pop("href", None)
        remote.pop("links", None)
        remote.pop("id", None)

        # Normalize fieldSchema - remove None values from remote to match desired
        # This is required as | is a shallow merge
        if "fieldSchema" in remote:
            for field in remote["fieldSchema"]:
                # Remove None/null values that won't be in desired (due to exclude_none=True)
                if field.get("displayName") is None:
                    field.pop("displayName", None)
                if field.get("description") is None:
                    field.pop("description", None)

        desired = self.model_dump(
            mode="json",
            exclude_none=True,
            by_alias=True,
            exclude={"dataset_id", "scope", "code"}
        )

        effective = remote | desired

        # If updated values are the same as what we currently have from API call
        # then we know we have nothing to update
        if effective == remote:
            return None

        client.put(
            f"/api/api/relationaldatasetdefinitions/{self.scope}/{self.code}",
            json=desired
        )
        return {
            "scope": self.scope,
            "code": self.code,
        }

    @staticmethod
    def delete(client: httpxClient, old_state: SimpleNamespace) -> None:
        client.delete(
            f"/api/api/relationaldatasetdefinitions/{old_state.scope}/{old_state.code}"
        )

    @classmethod
    def create_resource_id(cls, data: dict) -> str:
        return f"relation-dataset-definition_{data['scope']}_{data['code']}".lower()

    @classmethod
    def list_endpoint(cls, **kwargs) -> str:
        return "/api/api/relationaldatasetdefinitions"

    @classmethod
    def fetch_endpoint(cls) -> str:
        return "/api/api/relationaldatasetdefinitions/{scope}/{code}"

    @classmethod
    def from_api(cls, data: dict[str, Any]) -> dict[str, Any]:
        data = data | data["id"]
        data.pop("href", None)
        data.pop("version", None)
        data.pop("links", None)
        data["id"] = cls.create_resource_id(data["id"])

        return data

    @classmethod
    def get_keys(cls, data: dict[str, Any]) -> dict[str, Any]:
        return data["id"]

    def deps(self) -> list[Resource | Ref]:
        # Collect any DataType dependencies from field schemas
        deps = []
        for field in self.field_schema:
            if isinstance(field.data_type_id, (Resource, Ref)):
                deps.append(field.data_type_id)
        return deps


# Serialization helpers for use as keys in other resources
def ser_dataset_key(value, info):
    if info.context and info.context.get("style") == "dump":
        return {"$ref": value.id}
    return {"scope": value.scope, "code": value.code}


def des_dataset_key(value, info):
    if not isinstance(value, dict):
        return value
    if info.context and info.context.get("$refs"):
        ref = info.context["$refs"][value["$ref"]]
        return ref
    return value


RelationalDatasetDefinitionKey = Annotated[
    RelationalDatasetDefinitionResource | RelationalDatasetDefinitionRef,
    BeforeValidator(des_dataset_key),
    PlainSerializer(ser_dataset_key),
    WithJsonSchema(
        {
            "type": "object",
            "properties": {"$ref": {"type": "string", "format": "Key.RelationalDatasetDefinition"}},
            "required": ["$ref"],
        }
    ),
]
