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

from pydantic import AliasChoices, BaseModel, ConfigDict, Field

from mcp_tracker.tracker.proto.types.base import (
    BaseTrackerEntity,
    NoneExcludedField,
    none_excluder,
)
from mcp_tracker.tracker.proto.types.mixins import CreatedMixin, CreatedUpdatedMixin
from mcp_tracker.tracker.proto.types.refs import (
    BaseReference,
    ComponentReference,
    IssueReference,
    IssueTypeReference,
    PriorityReference,
    SprintReference,
    StatusReference,
    UserReference,
)


class Issue(CreatedUpdatedMixin, BaseTrackerEntity):
    model_config = ConfigDict(
        extra="allow",
    )
    version: int | None = NoneExcludedField
    unique: str | None = NoneExcludedField
    key: str | None = NoneExcludedField
    summary: str | None = NoneExcludedField
    description: str | None = NoneExcludedField
    type: IssueTypeReference | None = NoneExcludedField
    priority: PriorityReference | None = NoneExcludedField
    assignee: UserReference | None = NoneExcludedField
    status: StatusReference | None = NoneExcludedField
    previous_status: StatusReference | None = Field(
        None,
        validation_alias=AliasChoices("previousStatus", "previous_status"),
        exclude_if=none_excluder,
    )
    deadline: datetime.date | None = NoneExcludedField
    components: list[ComponentReference] | None = NoneExcludedField
    start: datetime.date | None = NoneExcludedField
    story_points: float | None = Field(
        None,
        validation_alias=AliasChoices("storyPoints", "story_points"),
        exclude_if=none_excluder,
    )
    tags: list[str] | None = NoneExcludedField
    votes: int | None = NoneExcludedField
    sprint: list[SprintReference] | None = NoneExcludedField
    epic: IssueReference | None = NoneExcludedField
    parent: IssueReference | None = NoneExcludedField
    estimation: str | None = NoneExcludedField
    spent: str | None = NoneExcludedField


IssueFieldsEnum = Enum(  # type: ignore[misc]
    "IssueFieldsEnum",
    {key: key for key in Issue.model_fields.keys()},
)


class IssueComment(CreatedUpdatedMixin, BaseTrackerEntity):
    id: int
    long_id: str | None = Field(
        None, validation_alias=AliasChoices("longId", "long_id")
    )
    text: str | None = None
    transport: str | None = None
    text_html: str | None = Field(
        None, validation_alias=AliasChoices("textHtml", "text_html")
    )
    summonees: list[UserReference] | None = Field(
        None,
        validation_alias=AliasChoices("summonees", "summonees"),
        exclude_if=none_excluder,
    )
    maillist_summonees: list["MaillistReference"] | None = Field(
        None,
        validation_alias=AliasChoices("maillistSummonees", "maillist_summonees"),
        exclude_if=none_excluder,
    )


class MaillistReference(BaseReference):
    display: str | None = None


class LinkTypeReference(BaseReference):
    id: str
    inward: str | None = None
    outward: str | None = None


# Link relationship types accepted by the Yandex Tracker "link issue" API.
IssueLinkRelationship = Literal[
    "relates",
    "is dependent by",
    "depends on",
    "is subtask for",
    "is parent task for",
    "duplicates",
    "is duplicated by",
    "is epic of",
    "has epic",
]


class IssueLink(CreatedUpdatedMixin, BaseTrackerEntity):
    id: int
    direction: str | None = None
    type: LinkTypeReference | None = None
    object: IssueReference | None = None
    assignee: UserReference | None = None
    status: StatusReference | None = None


class Worklog(CreatedUpdatedMixin, BaseTrackerEntity):
    id: int
    start: datetime.datetime | None = None
    duration: datetime.timedelta | None = None
    issue: IssueReference | None = None
    comment: str | None = None


class IssueAttachment(CreatedMixin, BaseTrackerEntity):
    id: str
    name: str
    content: str | None = None
    size: int | None = None
    mimetype: str | None = Field(
        None, validation_alias=AliasChoices("mimeType", "mimetype")
    )
    metadata: dict[str, str] | None = None


class ChecklistItemDeadline(BaseModel):
    date: datetime.datetime
    deadline_type: str = Field(
        validation_alias=AliasChoices("deadlineType", "deadline_type")
    )
    is_exceeded: bool = Field(
        validation_alias=AliasChoices("isExceeded", "is_exceeded")
    )


class ChecklistItem(BaseTrackerEntity):
    id: str
    text: str
    text_html: str | None = Field(
        None, validation_alias=AliasChoices("textHtml", "text_html")
    )
    checked: bool = False
    assignee: UserReference | None = None
    deadline: ChecklistItemDeadline | None = None
    checklist_item_type: str | None = Field(
        None, validation_alias=AliasChoices("checklistItemType", "checklist_item_type")
    )


class IssueTransition(BaseTrackerEntity):
    """Represents a possible status transition for an issue."""

    id: str
    display: str | None = None
    to: StatusReference | None = None


class ChangelogFieldReference(BaseReference):
    """Reference to the field that changed in a changelog entry (id + human-readable display)."""

    display: str | None = None


class ChangelogFieldChange(BaseTrackerEntity):
    """A single field change within a changelog entry (old value -> new value).

    `from`/`to` are intentionally untyped: Yandex Tracker returns a reference object
    for relation fields (status, assignee, ...), a plain string for text fields
    (summary, description, ...) or an array for collection fields (tags, components, ...).
    """

    field: ChangelogFieldReference | None = None
    from_: Any | None = Field(
        None,
        validation_alias=AliasChoices("from", "from_"),
        serialization_alias="from",
        exclude_if=none_excluder,
    )
    to: Any | None = NoneExcludedField


class ChangelogReference(BaseReference):
    """Reference to a sub-object (comment, trigger, ...) in a changelog entry (id + display)."""

    display: str | None = None


class ChangelogComments(BaseTrackerEntity):
    """Comment changes captured in a changelog entry (e.g. comments added).

    Modeled leniently (`extra="allow"`) so sibling keys the API may attach
    (`removed`, `changed`, ...) survive instead of being dropped.
    """

    model_config = ConfigDict(extra="allow")

    added: list[ChangelogReference] | None = NoneExcludedField


class ChangelogExecutedTrigger(BaseTrackerEntity):
    """A trigger executed as part of a changelog entry: which automation fired and its outcome."""

    trigger: ChangelogReference | None = None
    success: bool | None = None
    message: str | None = None


class ChangelogEntry(CreatedUpdatedMixin, BaseTrackerEntity):
    """A single entry of an issue change history: status transitions, field edits,
    comment changes, executed triggers, etc.

    `extra="allow"` so that documented top-level payloads the API attaches to specific
    change types but that are not modeled explicitly here (worklog/attachment/link/vote
    changes) pass through to the client instead of being silently dropped, matching the
    `Issue` model's passthrough behavior.
    """

    model_config = ConfigDict(extra="allow")

    id: str
    issue: IssueReference | None = None
    type: str | None = None
    transport: str | None = None
    fields: list[ChangelogFieldChange] | None = None
    comments: ChangelogComments | None = NoneExcludedField
    executed_triggers: list[ChangelogExecutedTrigger] | None = Field(
        None,
        validation_alias=AliasChoices("executedTriggers", "executed_triggers"),
        exclude_if=none_excluder,
    )


class ChangelogPage(BaseTrackerEntity):
    """A page of issue changelog entries plus the cursor to fetch the next page.

    `next_cursor` is parsed from the `Link: rel="next"` response header; it is `None`
    when there are no more pages. Pass it back as the `cursor` argument to continue.
    """

    entries: list[ChangelogEntry]
    next_cursor: str | None = None
