import abc
from datetime import date
from decimal import Decimal
from typing import Any, Protocol, TypedDict, Union, cast

from pydantic import BaseModel, RootModel

from app.common.filters import format_date, format_date_approximate

NOT_ASKED = "NOT_ASKED"
NOT_ANSWERED = "NOT_ANSWERED"


class ChoiceDict(TypedDict):
    key: str
    label: str


class SubmissionAnswerProtocol(Protocol):
    # All classes in this module must implement this protocol. For now, we can't tie this up to type hinting nicely -
    # waiting on Python to support type intersections so that we say that all answers must be BaseModel &
    # AnswerProtocol. So for now - developers will need to manually keep this lined up.

    _render_answer_template: str

    def get_value_for_submission(self) -> Any: ...
    def get_value_for_form(self) -> Any: ...
    def get_value_for_evaluation(self) -> Any: ...
    def get_value_for_interpolation(self) -> str: ...
    def get_value_for_text_export(self) -> str: ...


class SubmissionAnswerRootModel[T](RootModel[T]):
    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/root.html"

    def get_value_for_submission(self) -> T:
        return cast(T, self.model_dump(mode="json"))

    def get_value_for_form(self) -> T:
        return self.root

    def get_value_for_evaluation(self) -> T:
        return self.root

    def get_value_for_interpolation(self) -> str:
        return str(self.root)

    def get_value_for_text_export(self) -> str:
        return str(self.root)

    def get_value_for_json_export(self) -> T:
        return cast(T, self.model_dump(mode="json"))


class SubmissionAnswerBaseModel(BaseModel, abc.ABC):
    @property
    @abc.abstractmethod
    def _render_answer_template(self) -> str: ...
    @abc.abstractmethod
    def get_value_for_submission(self) -> Any: ...
    @abc.abstractmethod
    def get_value_for_form(self) -> Any: ...
    @abc.abstractmethod
    def get_value_for_evaluation(self) -> Any: ...
    @abc.abstractmethod
    def get_value_for_interpolation(self) -> str: ...
    @abc.abstractmethod
    def get_value_for_text_export(self) -> str: ...
    @abc.abstractmethod
    def get_value_for_json_export(self) -> Any: ...


class TextSingleLineAnswer(SubmissionAnswerRootModel[str]):
    pass


class TextMultiLineAnswer(SubmissionAnswerRootModel[str]):
    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/text_multi_line.html"


class EmailAnswer(SubmissionAnswerRootModel[str]):
    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/email.html"


class UrlAnswer(SubmissionAnswerRootModel[str]):
    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/url.html"


class YesNoAnswer(SubmissionAnswerRootModel[bool]):
    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/yes_no.html"

    def get_value_for_submission(self) -> bool:
        return cast(bool, self.model_dump(mode="json"))

    def get_value_for_text_export(self) -> str:
        return "Yes" if self.root else "No"

    def get_value_for_interpolation(self) -> str:
        return "Yes" if self.root else "No"

    def get_value_for_json_export(self) -> bool:
        return cast(bool, self.model_dump(mode="json"))


class _NumberAnswer[T: int | Decimal](SubmissionAnswerBaseModel):
    value: T
    prefix: str | None = None
    suffix: str | None = None

    def model_post_init(self, __context: Any) -> None:
        if type(self).__pydantic_generic_metadata__["parameters"]:
            raise TypeError(
                "Cannot instantiate _NumberAnswer directly. Use a concrete type like IntegerAnswer or DecimalAnswer."
            )

    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/number.html"

    def get_value_for_interpolation(self) -> str:
        return self.get_value_for_text_export()

    def get_value_for_submission(self) -> dict[str, Any]:
        return self.model_dump(mode="json", exclude_none=True)

    def get_value_for_form(self) -> T:
        return self.value

    def get_value_for_evaluation(self) -> T:
        return self.value

    def get_value_for_text_export(self) -> str:
        return f"{self.prefix or ''}{self.value:,}{self.suffix or ''}"

    def get_value_for_json_export(self) -> dict[str, str | T]:
        data: dict[str, str | T] = {"value": self.value}

        if self.prefix:
            data["prefix"] = self.prefix
        if self.suffix:
            data["suffix"] = self.suffix

        return data


IntegerAnswer = _NumberAnswer[int]


class DecimalAnswer(_NumberAnswer[Decimal]):
    def get_value_for_json_export(self) -> dict[str, str | Decimal]:
        data: dict[str, str | Decimal] = {"value": f"{self.value:}"}

        if self.prefix:
            data["prefix"] = self.prefix
        if self.suffix:
            data["suffix"] = self.suffix

        return data


class SingleChoiceFromListAnswer(SubmissionAnswerBaseModel):
    """
    NOTE: If the data structure changes, we may also need to sync/update the following places:
    - Submission.name (for multi-submissions)
    """

    key: str
    label: str

    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/single_choice_from_list.html"

    def get_value_for_submission(self) -> dict[str, Any]:
        return self.model_dump(mode="json")

    def get_value_for_form(self) -> str:
        return self.key

    def get_value_for_evaluation(self) -> str:
        return self.key

    def get_value_for_interpolation(self) -> str:
        return self.label

    def get_value_for_text_export(self) -> str:
        return self.label

    def get_value_for_json_export(self) -> ChoiceDict:
        return {"key": self.key, "label": self.label}


class MultipleChoiceFromListAnswer(SubmissionAnswerBaseModel):
    choices: list[ChoiceDict]

    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/multiple_choice_from_list.html"

    def get_value_for_submission(self) -> dict[str, Any]:
        return self.model_dump(mode="json")

    def get_value_for_form(self) -> list[str]:
        return [choice["key"] for choice in self.choices]

    def get_value_for_evaluation(self) -> set[str]:
        return {choice["key"] for choice in self.choices}

    def get_value_for_interpolation(self) -> str:
        # This feels like quite an iffy representation for interpolation; I'm not sure what we should _actually_ do here
        return ", ".join(choice["label"] for choice in self.choices)

    def get_value_for_text_export(self) -> str:
        return "\n".join(choice["label"] for choice in self.choices)

    def get_value_for_json_export(self) -> list[ChoiceDict]:
        return self.choices


class DateAnswer(SubmissionAnswerBaseModel):
    answer: date
    approximate_date: bool = False

    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/date.html"

    def get_value_for_submission(self) -> dict[str, Any]:
        return self.model_dump(mode="json")

    def get_value_for_form(self) -> date:
        return self.answer

    def get_value_for_evaluation(self) -> date:
        return self.answer

    def get_value_for_interpolation(self) -> str:
        return format_date(self.answer) if not self.approximate_date else format_date_approximate(self.answer)

    def get_value_for_text_export(self) -> str:
        return self.answer.isoformat() if not self.approximate_date else self.answer.strftime("%B %-Y")

    def get_value_for_json_export(self) -> str:
        return self.answer.isoformat() if not self.approximate_date else self.answer.strftime("%B %-Y")


class FileUploadAnswer(SubmissionAnswerBaseModel):
    filename: str
    key: str | None = None
    size: int
    mime_type: str
    # for now we'll only track if it has been scanned for viruses and will clear the answer if it
    # failed the check, this will allow us to not enable downloads until we've scanned the file
    # but in the future we may choose to not delete the answer and store the check here to show
    # represent the check in the UI
    scanned_for_viruses: bool = False

    @property
    def _render_answer_template(self) -> str:
        return "common/partials/answers/file_upload.html"

    def get_value_for_submission(self) -> dict[str, Any]:
        return self.model_dump(mode="json")

    def get_value_for_form(self) -> str:
        return self.filename

    def get_value_for_evaluation(self) -> str:
        return self.filename

    def get_value_for_interpolation(self) -> str:
        # todo: when interpolating file upload generating a link to the download file would allow markdown to format it
        return self.filename

    def get_value_for_text_export(self) -> str:
        return self.filename

    def get_value_for_json_export(self) -> str:
        # todo: we can expose more information in the JSON export when persisting more
        return self.filename


AllAnswerTypes = Union[
    TextSingleLineAnswer
    | TextMultiLineAnswer
    | IntegerAnswer
    | DecimalAnswer
    | EmailAnswer
    | UrlAnswer
    | YesNoAnswer
    | SingleChoiceFromListAnswer
    | MultipleChoiceFromListAnswer
    | DateAnswer
    | FileUploadAnswer
]

DataSourceAnswerTypes = TextSingleLineAnswer | IntegerAnswer | DecimalAnswer
