import json
from enum import StrEnum
from types import SimpleNamespace
from typing import Any, Dict, List, Optional, Self, Sequence, Union

from httpx import Client as httpxClient
from pydantic import BaseModel, Field, field_serializer, model_validator

from .property import DefinitionRef, DefinitionResource
from .resource_abc import Ref, Resource, register_resource
from .side_definition import SideRef, SideResource


class TransactionTypeAlias(BaseModel):
    type: str
    description: str
    transaction_class: str = Field(serialization_alias="transactionClass")
    transaction_roles: str = Field(serialization_alias="transactionRoles")
    is_default: Optional[bool] = Field(False, serialization_alias="isDefault")


class MovementOption(StrEnum):
    DirectAdjustment = "DirectAdjustment"
    IncludesTradedInterest = "IncludesTradedInterest"


class MovementType(StrEnum):
    Settlement = "Settlement"
    Traded = "Traded"
    StockMovement = "StockMovement"
    FutureCash = "FutureCash"
    Commitment = "Commitment"
    Receivable = "Receivable"
    CashSettlement = "CashSettlement"
    CashForward = "CashForward"
    CashCommitment = "CashCommitment"
    CashReceivable = "CashReceivable"
    Accrual = "Accrual"
    CashAccrual = "CashAccrual"
    ForwardFx = "ForwardFx"
    CashFxForward = "CashFxForward"
    UnsettledCashTypes = "UnsettledCashTypes"
    # Carry movements - Memo transaction to record carry against an instrument.
    Carry = "Carry"  # Carry that will present as an inflow/outflow.
    CarryAsPnl = "CarryAsPnl"  # Carry that will present as a gain or loss.
    VariationMargin = "VariationMargin"
    Capital = "Capital"
    Fee = "Fee"


class CalculationType(StrEnum):
    TaxAmounts = "TaxAmounts"
    NotionalAmount = "Txn:NotionalAmount"
    GrossConsideration = "Txn:GrossConsideration"
    TradeToPortfolioRate = "Txn:TradeToPortfolioRate"
    ExchangeRate = "Txn:ExchangeRate"
    DeriveTotalConsideration = "DeriveTotalConsideration"
    BondInterest = "Txn:BondInterest"


class TransactionTypeCalculation(BaseModel):
    type: CalculationType | str
    side: SideRef | SideResource | None = None
    formula: Optional[str] = None

    @field_serializer("side")
    def serialize_side(self, v: SideRef | SideResource | None):
        if v is not None:
            return v.side
        return None


class MetricValue(BaseModel):
    value: float
    unit: Optional[str] = None


class PerpetualProperty(BaseModel):
    property_key: Union[DefinitionResource, DefinitionRef] = Field(serialization_alias="propertyKey")
    label_value: Optional[str] = Field(default=None, serialization_alias="labelValue")
    metric_value: Optional[MetricValue] = Field(default=None, serialization_alias="metricValue")
    label_set_value: Optional[List[str]] = Field(default=None, serialization_alias="labelSetValue")

    @model_validator(mode="after")
    def validate_one_value_exists(self) -> Self:
        fields = ["label_value", "metric_value", "label_set_value"]
        s = [field for field in fields if getattr(self, field) is not None]
        if len(s) > 1:
            raise KeyError(f"Cannot set {' and '.join(s)}, only one of {' or '.join(fields)} can be set")

        return self


class TransactionTypePropertyMapping(BaseModel):
    property_key: Union[DefinitionResource, DefinitionRef] = Field(serialization_alias="propertyKey")
    map_from: Optional[Union[DefinitionResource, DefinitionRef]] = Field(
        serialization_alias="mapFrom", default=None
    )
    set_to: str = Field(serialization_alias="setTo", default=None)

    @field_serializer("property_key", "map_from", when_used="always")
    def serialize_property_key(self, v: Union[DefinitionRef, DefinitionResource]):
        return f"{v.domain.value}/{v.scope}/{v.code}"


def sort_transaction_type_arrays(obj):  # type: ignore
    if isinstance(obj, dict):
        return {k: sort_transaction_type_arrays(v) for k, v in sorted(obj.items())}
    elif isinstance(obj, list):
        if all(isinstance(i, dict) for i in obj):
            return sorted(
                [sort_transaction_type_arrays(i) for i in obj],
                key=lambda x: json.dumps(x, sort_keys=True),
            )
        else:
            return sorted(obj)
    else:
        return obj


def compare_json_structures(json1, json2) -> bool:
    return sort_transaction_type_arrays(json1) == sort_transaction_type_arrays(json2)


def serialize_properties(values: List[PerpetualProperty]):
    property_dict = {}

    for prop in values:
        value = {}
        if prop.label_value:
            value["labelValue"] = prop.label_value
        elif prop.metric_value:
            value["metricValue"] = prop.metric_value
        elif prop.label_set_value:
            value["labelSetValue"] = prop.label_set_value

        key = f"{prop.property_key.domain.value}/{prop.property_key.scope}/{prop.property_key.code}"
        property_dict[key] = {"key": key, "value": value}

    return property_dict


class TransactionTypeMovement(BaseModel):
    movement_types: MovementType = Field(serialization_alias="movementTypes")
    side: SideRef | SideResource
    direction: int
    properties: Optional[List[PerpetualProperty]] = []
    mappings: Optional[List[TransactionTypePropertyMapping]] = []
    name: Optional[str] = None
    movement_options: Optional[List[MovementOption] | str] = Field(
        default=[], serialization_alias="movementOptions"
    )
    settlement_date_override: Optional[str] = Field(
        default=None, serialization_alias="settlementDateOverride"
    )
    condition: Optional[str] = ""

    @field_serializer("side", when_used="always")
    def serialize_side(self, v: SideRef | SideResource):
        return v.side

    @field_serializer("properties", when_used="always")
    def serialize_properties(self, v: List[PerpetualProperty]):
        return serialize_properties(v)


@register_resource()
class TransactionTypeResource(BaseModel, Resource):
    """
    Represents a transaction type
    """

    id: str = Field(exclude=True, init=True)
    scope: str = Field(exclude=True, init=True)
    source: str = Field(exclude=True, init=True)
    aliases: List[TransactionTypeAlias] = Field(min_length=1)
    movements: List[TransactionTypeMovement] = []
    properties: Optional[List[PerpetualProperty]] = []
    calculations: Optional[List[TransactionTypeCalculation]] = []

    def _get_first_alias(self):
        return sorted([alias.type for alias in self.aliases])[0]

    @field_serializer("properties", when_used="always")
    def serialize_properties(self, v: List[PerpetualProperty]):
        return serialize_properties(v)

    def read(self, client: httpxClient, old_state: SimpleNamespace) -> None | Dict[str, Any]:
        transaction_type = old_state.transaction_type
        source = old_state.source
        scope = old_state.scope
        response = client.get(
            f"/api/api/transactionconfiguration/types/{source}/{transaction_type}",
            params={"scope": scope},
        ).json()
        if "links" in response.keys():
            del response["links"]
        return response

    def create(self, client: httpxClient) -> Optional[Dict[str, Any]]:
        desired = self.model_dump(mode="json", exclude_none=True, by_alias=True)
        client.put(
            f"/api/api/transactionconfiguration/types/{self.source}/{self._get_first_alias()}",
            params={"scope": self.scope},
            json=desired,
        )
        return {"source": self.source, "scope": self.scope, "transaction_type": self._get_first_alias()}

    def update(self, client: httpxClient, old_state) -> Union[None, Dict[str, Any]]:
        if [old_state.transaction_type, old_state.scope, old_state.source] != [
            self._get_first_alias(),
            self.scope,
            self.source,
        ]:
            self.delete(client, old_state)
            return self.create(client)

        remote = self.read(client, old_state) or {}
        desired = self.model_dump(mode="json", exclude_none=True, by_alias=True)

        if compare_json_structures(remote, desired):
            return None

        return self.create(client)

    @staticmethod
    def delete(client: httpxClient, old_state) -> None:
        transaction_type, scope, source = (old_state.transaction_type, old_state.scope, old_state.source)
        client.delete(
            f"/api/api/transactionconfiguration/types/{source}/{transaction_type}",
            params={"scope": scope},
        )

    def deps(self) -> Sequence[Resource | Ref]:
        def add_to_unique(item):
            if item.id not in seen:
                seen.add(item.id)
                unique.append(item)

        seen = set()
        unique = []

        for movement in self.movements or []:
            if isinstance(movement.side, (SideRef, SideResource)):
                add_to_unique(movement.side)
            for p in movement.properties or []:
                add_to_unique(p.property_key)
            for m in movement.mappings or []:
                add_to_unique(m.property_key)
                if m.map_from:
                    add_to_unique(m.map_from)

        for prop in self.properties or []:
            add_to_unique(prop.property_key)

        for calc in self.calculations or []:
            if calc.side is not None:
                add_to_unique(calc.side)

        return unique
