from __future__ import annotations

import copy
import json
from enum import StrEnum
from hashlib import sha256
from typing import Annotated, Any, ClassVar, Dict, List

import httpx
from pydantic import (
    BaseModel,
    BeforeValidator,
    Field,
    PlainSerializer,
    WithJsonSchema,
    model_serializer,
    model_validator,
)

from . import property  # type: noqa
from .properties import MetricValue, PropertyValue, PropertyValueDict
from .resource_abc import CamelAlias, FetchMixin, ListMixin, Ref, Resource, register_resource
from .urlfmt import Urlfmt

# force these to be used so they appear in the customentity namespace
_ = [property, PropertyValue, MetricValue]

# Context key for reference resolution
REFS_CONTEXT_KEY = "$refs"
GROUP = "Data Model"


class CollectionType(StrEnum):
    SINGLE = "Single"
    ARRAY = "Array"


class LifeTime(StrEnum):
    PERPETUAL = "Perpetual"
    TIMEVARIANT = "TimeVariant"


class FieldType(StrEnum):
    STRING = "String"
    BOOLEAN = "Boolean"
    DATE_TIME = "DateTime"
    DECIMAL = "Decimal"


class FieldDefinition(CamelAlias, BaseModel):
    name: str
    lifetime: LifeTime
    type: FieldType
    collection_type: CollectionType = CollectionType.SINGLE
    required: bool
    description: str = ""


# These are optional in the API create and will be given default values. When read is called
# they will not be returned if they have the default value
DEFAULT_FIELD = {"collectionType": "Single", "description": ""}


@register_resource(group=GROUP, keys=["entityType"])
class EntityTypeResource(CamelAlias, BaseModel, Resource, ListMixin, FetchMixin):
    id: str = Field(exclude=True)
    entity_type_name: str
    display_name: str
    description: str
    field_schema: List[FieldDefinition]

    @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

    def read(self, client, old_state) -> Dict[str, Any]:
        entity_type = old_state.entitytype
        return client.request("get", f"/api/api/customentities/entitytypes/{entity_type}").json()

    def create(self, client: httpx.Client):
        desired = self.model_dump(mode="json", exclude_none=True, by_alias=True)
        res = client.request("POST", "/api/api/customentities/entitytypes", json=desired).json()
        return {"entitytype": res["entityType"]}

    def update(self, client: httpx.Client, old_state):
        remote = self.read(client, old_state)
        # enrich remote fields with the default values if not present
        remote["fieldSchema"] = [rem | DEFAULT_FIELD for rem in remote["fieldSchema"]]
        desired = self.model_dump(mode="json", exclude_none=True, by_alias=True)
        effective = remote | copy.deepcopy(desired)
        for i in range(0, len(self.field_schema)):
            if i < len(remote["fieldSchema"]):
                eff_field = remote["fieldSchema"][i] | desired["fieldSchema"][i]
                effective["fieldSchema"][i] = eff_field
        if effective == remote:
            return None
        res = client.request(
            "PUT", f"/api/api/customentities/entitytypes/{old_state.entitytype}", json=desired
        ).json()
        return {"entitytype": res["entityType"]}

    @staticmethod
    def delete(client, old_state):
        raise RuntimeError("Cannot delete a custom entity definition")

    @classmethod
    def create_resource_id(cls, data: dict) -> str:
        return f"entity-type_{data['entityType']}".lower()

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

    @classmethod
    def from_api(cls, data: dict[str, Any]) -> dict[str, Any]:
        result = {
            "id": cls.create_resource_id(data),
            "entityTypeName": data["entityTypeName"],
            "displayName": data["displayName"],
            "description": data["description"],
            "fieldSchema": []
        }

        # Process fieldSchema, removing default values
        for field in data.get("fieldSchema", []):
            field_data = {
                "name": field["name"],
                "lifetime": field["lifetime"],
                "type": field["type"],
                "required": field["required"]
            }
            # Only include non-default values
            if field.get("collectionType") and field.get("collectionType") != "Single":
                field_data["collectionType"] = field["collectionType"]
            if field.get("description", "") != "":
                field_data["description"] = field["description"]

            result["fieldSchema"].append(field_data)

        return result

    @classmethod
    def fetch_endpoint(cls) -> str:
        return "/api/api/customentitytypes/{entityType}"

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

    def deps(self):
        return []


@register_resource(group=GROUP)
class EntityTypeRef(CamelAlias, BaseModel, Ref):
    id: str = Field(exclude=True)
    entity_type_name: str

    def attach(self, client):
        entity_type = self.entity_type_name
        try:
            client.get(f"/api/api/customentities/entitytypes/{entity_type}").json()
        except httpx.HTTPStatusError as ex:
            if ex.response.status_code == 404:
                raise RuntimeError(f"Custom Entity Definition {entity_type} does not exist")
            else:
                raise ex


def ser_entitytype_key(value, info):
    if info.context and info.context.get("style", "api") == "dump":
        return {"$ref": value.id}
    return "~" + value.entity_type_name


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


EntityTypeKey = Annotated[
    EntityTypeResource | EntityTypeRef,
    BeforeValidator(des_entitytype_key),
    PlainSerializer(ser_entitytype_key),
    WithJsonSchema(
        {
            "type": "object",
            "properties": {"$ref": {"type": "string", "format": "Key.EntityType"}},
            "required": ["$ref"],
        }
    ),
]


class EntityField(CamelAlias, BaseModel):
    effective_from: str | None = None
    effective_to: str | None = None
    name: str
    value: Any


class EntityIdentifier(CamelAlias, BaseModel):
    effective_from: str | None = None
    effective_to: str | None = None
    identifier_type: property.DefinitionRef | property.DefinitionResource
    identifier_value: str

    @model_serializer()
    def ser_model(self, info):
        style = info.context.get("style", "api") if info.context else "api"
        if style == "dump":
            return {
                "identifierType": {"$ref": self.identifier_type.id},
                "identifierValue": self.identifier_value,
            }
        return {
            "identifierScope": self.identifier_type.scope,
            "identifierType": self.identifier_type.code,
            "identifierValue": self.identifier_value
        }

    @model_validator(mode="before")
    @classmethod
    def des_model(cls, data, info):
        if not isinstance(data, dict):
            return data
        if info.context and info.context.get(REFS_CONTEXT_KEY):
            ident_type = data["identifierType"]
            if isinstance(ident_type, dict) and "$ref" in ident_type:
                ref_id = ident_type["$ref"]
                return data | {"identifierType": info.context[REFS_CONTEXT_KEY][ref_id]}
            elif isinstance(ident_type, str):
                return data | {"identifierType": info.context[REFS_CONTEXT_KEY][ident_type]}
        return data


@register_resource(
    group=GROUP,
    parent=EntityTypeResource,
    keys=["entityType", "identifierType", "identifierValue", "identifierScope"]
)
class EntityResource(CamelAlias, BaseModel, Resource, ListMixin, FetchMixin):
    id: str = Field(exclude=True)
    fields: List[EntityField]
    identifiers: List[EntityIdentifier] = Field(min_length=1)
    entity_type: EntityTypeKey
    description: str
    display_name: str
    properties: PropertyValueDict | None = None

    u: ClassVar = Urlfmt("/api/api/customentities")

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

    def create(self, client):
        desired = self.model_dump(mode="json", exclude_none=True, by_alias=True, exclude={"entity_type"})
        sorted_desired = json.dumps(desired, sort_keys=True)
        content_hash = sha256(sorted_desired.encode()).hexdigest()
        i0 = desired["identifiers"][0]
        url = self.u.format("{base}/~{type}", type=self.entity_type.entity_type_name)
        client.post(url, json=desired)
        return {
            "entity_type": self.entity_type.entity_type_name,
            "identifier_scope": i0["identifierScope"],
            "identifier_type": i0["identifierType"],
            "identifier_value": i0["identifierValue"],
            "content_hash": content_hash
        }

    def read(self, client, old_state) -> Dict:
        url = self.u.format("{base}/~{type}/{idtype}/{idvalue}",
            type=old_state.entity_type,
            idtype=old_state.identifier_type,
            idvalue=old_state.identifier_value,
        )
        res = client.get(url, params={"identifierScope": old_state.identifier_scope}).json()
        res.pop("href", None)
        res.pop("version", None)
        res.pop("relationships", None)
        return res

    def update(self, client, old_state):
        desired = self.model_dump(mode="json", exclude_none=True, by_alias=True,
                                  exclude={"id", "scope", "entity_type"})
        sorted_desired = json.dumps(desired, sort_keys=True)
        content_hash = sha256(sorted_desired.encode()).hexdigest()
        if old_state.content_hash == content_hash:
            return None
        # if the type or previously used identifier has changes delete and recreate
        newid = [
            i for i in desired["identifiers"]
             if i["identifierType"] == old_state.identifier_type and
                i["identifierScope"] == old_state.identifier_scope and
                i["identifierValue"] == old_state.identifier_value
        ]
        if len(newid) != 1 or self.entity_type.entity_type_name != old_state.entity_type:
            self.delete(client, old_state)
            return self.create(client)
        url = self.u.format("{base}/~{type}", type=self.entity_type.entity_type_name)
        client.post(url, json=desired)
        i0 = desired["identifiers"][0]
        return {
            "entity_type": self.entity_type.entity_type_name,
            "identifier_scope": i0["identifierScope"],
            "identifier_type": i0["identifierType"],
            "identifier_value": i0["identifierValue"],
            "content_hash": content_hash
        }

    @staticmethod
    def delete(client, old_state):
        url = EntityResource.u.format("{base}/~{type}/{idtype}/{idvalue}",
            type=old_state.entity_type,
            idtype=old_state.identifier_type,
            idvalue=old_state.identifier_value,
        )
        client.delete(url, params={"identifierScope": old_state.identifier_scope})

    def deps(self):
        res = [self.entity_type] + [i.identifier_type for i in self.identifiers]
        if self.properties:
            res.extend([p.property_key for p in self.properties])
        return res

    @classmethod
    def list_endpoint(cls, **kwargs) -> str:
        return "/api/api/customentities/{entityType}"

    @classmethod
    def fetch_endpoint(cls) -> str:
        return "/api/api/customentities/{entityType}/{identifierType}/{identifierValue}"

    @classmethod
    def create_resource_id(cls, data: dict) -> str:
        identifier = data["identifiers"][0]
        id_parts = "_".join(
            identifier[key] for key in ("identifierType", "identifierValue", "identifierScope")
        )
        return f"entity_{data['entityType']}_{id_parts}".lower()

    @classmethod
    def get_keys(cls, data: dict[str, Any]) -> dict[str, Any]:
        identifier = data["identifiers"][0]
        return {
            "identifierType": identifier["identifierType"],
            "identifierValue": identifier["identifierValue"],
            "identifierScope": identifier["identifierScope"],
            "entityType": data["entityType"]
        }

    @classmethod
    def from_api(cls, data: dict[str, Any]) -> dict[str, Any]:
        data.pop("href", None)
        data.pop("links", None)
        data.pop("version", None)
        data.pop("relationships", None)
        data.pop("stagedModifications", None)
        data.pop("identifierScope", None)
        data.pop("identifierType", None)
        data.pop("identifierValue", None)

        data["id"] = cls.create_resource_id(data)

        entity_type = data.pop("entityType")
        data["entityType"] = {
            "$ref": f"entity-type_{entity_type}".lower()
        }

        identifier = data.pop("identifiers")[0]
        data["identifiers"] = [{
            "identifierType": {
                "$ref": f"identifier-type_{identifier['identifierType']}".lower()
            },
            "identifierValue": identifier["identifierValue"],
            "effectiveFrom": identifier["effectiveFrom"],
            "effectiveTo": identifier["effectiveUntil"]
        }]

        for field in data["fields"]:
            field["effectiveTo"] = field.pop("effectiveUntil")

        return data
