import datetime
from typing import List, Literal
from enum import Enum

from pydantic import BaseModel, Field, validator
from beanie.operators import ElemMatch

from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
from ..entity.models import Entity


class ContractStatus(str, Enum):
    published = 'published'
    signed = 'signed'
    printed = 'printed'
    executed = 'executed'


class ContractDraftStatus(str, Enum):
    in_progress = 'in_progress'
    ready = 'ready'
    published = 'published'


class DraftParty(BaseModel):
    entity_id: str = Field(
        foreignKey={
            "reference": {
                "resource": "entity",
                "schema": "Entity",
            }
        },
        default="",
        title="Partie"
    )
    part: str = Field(title="Rôle")
    representative_id: str = Field(
        foreignKey={
            "reference": {
                "resource": "entity",
                "schema": "Entity",
            }
        },
        default="",
        title="Représentant"
    )

    class Config:
        title = 'Partie'


class Party(BaseModel):
    entity: Entity
    part: str
    representative: Entity = None
    signature_uuid: str
    signature_affixed: bool = False
    signature_png: str = None


class ProvisionGenuine(BaseModel):
    type: Literal['genuine'] = 'genuine'
    title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
    body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")

    class Config:
        title = 'Clause personalisée'


class ContractProvisionTemplateReference(BaseModel):
    type: Literal['template'] = 'template'
    provision_template_id: str = Field(
        foreignKey={
            "reference": {
                "resource": "template/provision",
                "schema": "ProvisionTemplate",
                "displayedFields": ['title', 'body']
            },
        },
        props={"parametrized": True},
        default="",
        title="Template de clause"
    )

    class Config:
        title = 'Template de clause'


class DraftProvision(BaseModel):
    provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')

    class Config:
        title = 'Clause'


class Provision(BaseModel):
    title: str = RichtextSingleline(title="Titre")
    body: str = RichtextMultiline(title="Corps")


class ContractDraft(CrudDocument):
    """
    Brouillon de contrat à remplir
    """

    name: str = Field(title="Nom")
    title: str = Field(title="Titre")
    parties: List[DraftParty] = Field(title="Parties")
    provisions: List[DraftProvision] = Field(
        props={"items-per-row": "1", "numbered": True},
        title='Clauses'
    )
    variables: List[DictionaryEntry] = Field(
        default=[],
        format="dictionary",
        title='Variables'
    )
    status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
    todo: List[str] = Field(default=[], title="Reste à faire")

    class Settings(CrudDocument.Settings):
        fulltext_search = ['name', 'title']

        bson_encoders = {
            datetime.date: lambda dt: dt if hasattr(dt, 'hour')
            else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
                                   hour=0, minute=0, second=0)
        }

    class Config:
        title = 'Brouillon de contrat'

    async def check_is_ready(self):
        if self.status == ContractDraftStatus.published:
            return

        self.todo = []
        if len(self.parties) < 2:
            self.todo.append('Contract must have at least two parties')
        if len(self.provisions) < 1:
            self.todo.append('Contract must have at least one provision')

        for p in self.parties:
            if not p.entity_id:
                self.todo.append('All parties must have an associated entity`')

        for p in self.provisions:
            if p.provision.type == "genuine" and not (p.provision.title and p.provision.body):
                self.todo.append('Empty genuine provision')
            elif p.provision.type == "template" and not p.provision.provision_template_id:
                self.todo.append('Empty template provision')

        for v in self.variables:
            if not (v.key and v.value):
                self.todo.append('Empty variable')

        if self.todo:
            self.status = ContractDraftStatus.in_progress
        else:
            self.status = ContractDraftStatus.ready

        await self.update({"$set": {
            "status": self.status,
            "todo": self.todo
        }})


class Contract(CrudDocument):
    """
    Contrat publié. Les contrats ne peuvent pas être modifiés.
    Ils peuvent seulement être signés par les parties et imprimés par l'avocat
    """
    name: str = Field(title="Nom")
    title: str = Field(title="Titre")
    parties: List[Party] = Field(title="Parties")
    provisions: List[Provision] = Field(
        props={"items-per-row": "1", "numbered": True},
        title='Clauses'
    )
    status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
    lawyer: Entity = Field(title="Avocat en charge")
    location: str = Field(title="Lieu")
    date: datetime.date = Field(title="Date")
    label: str = None

    @validator("label", always=True)
    def generate_label(cls, v, values, **kwargs):
        if not v:
            contract_label = values['title']
            for p in values['parties']:
                contract_label = contract_label + f" - {p.entity.label}"

            contract_label = contract_label + f" {values['date'].strftime('%m/%d/%Y')}"
            return contract_label

        return v

    class Settings(CrudDocument.Settings):
        fulltext_search = ['name', 'title']

        bson_encoders = {
            datetime.date: lambda dt: dt if hasattr(dt, 'hour')
            else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
                                   hour=0, minute=0, second=0)
        }

    @classmethod
    def find_by_signature_id(cls, signature_id: str):
        crit = ElemMatch(cls.parties, {"signature_uuid": signature_id})
        return cls.find_one(crit)

    def get_signature(self, signature_id: str):
        for p in self.parties:
            if p.signature_uuid == signature_id:
                return p

    def get_signature_index(self, signature_id: str):
        for i, p in enumerate(self.parties):
            if p.signature_uuid == signature_id:
                return i

    def is_signed(self):
        for p in self.parties:
            if not p.signature_affixed:
                return False
        return True


def replace_variables_in_value(variables, value: str):
    for v in variables:
        if v.value:
            value = value.replace('%{}%'.format(v.key), v.value)
    return value
