import base64
import datetime
from abc import abstractmethod
from collections.abc import Callable
from typing import Any, Literal

import msgspec
import streamlit as st
from streamlit.runtime.uploaded_file_manager import UploadedFile

from csirtmu.self_service.enums import NetboxQueryType
from csirtmu.self_service.structs import FormData, ImageAttachment


class Base(msgspec.Struct):
    @abstractmethod
    def display(self) -> None: ...


class Step(Base):
    id: int
    title: str
    description: str | None

    def display(self) -> None:
        st.markdown("## " + self.title)
        if self.description is not None:
            st.markdown(self.description)


class TextStep(Step, tag="TextStep", tag_field="kind"):
    text: str

    def display(self) -> None:
        super().display()
        st.markdown(self.text)


class LinkStep(Step, tag="LinkStep", tag_field="kind"):
    link: str

    def display(self) -> None:
        super().display()
        st.markdown(f"You may find more information about this here: [{self.link}]({self.link})")


class FormField(msgspec.Struct):
    label: str
    position: int

    @property
    @abstractmethod
    def kwarg_names(self) -> list[str]: ...

    @abstractmethod
    def display(self, key: str | None = None) -> Any: ...

    def get_kwarg_values(self) -> dict[str, Any]:
        return {kwarg: value for kwarg in self.kwarg_names if (value := getattr(self, kwarg)) is not None}


class TextInput(FormField, tag="TextInput", tag_field="kind"):
    value: str | None = None
    max_chars: int | None = None
    type: Literal["default", "password"] = "default"
    help: str | None = None
    autocomplete: str | None = None
    placeholder: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"
    icon: str | None = None

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "value",
            "max_chars",
            "type",
            "help",
            "autocomplete",
            "placeholder",
            "label_visibility",
            "icon",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.text_input(**self.get_kwarg_values())
        return st.text_input(**self.get_kwarg_values(), key=key + self.label)


class Selectbox(FormField, tag="Selectbox", tag_field="kind"):
    options: list[str]
    index: int | None = None
    help: str | None = None
    placeholder: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"
    accept_new_options: bool = False

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "options",
            "index",
            "help",
            "placeholder",
            "label_visibility",
            "accept_new_options",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.selectbox(**self.get_kwarg_values())
        return st.selectbox(**self.get_kwarg_values(), key=key + self.label)


class Multiselect(FormField, tag="Multiselect", tag_field="kind"):
    options: list[str]
    default: list[str] | None = None
    help: str | None = None
    max_selections: int | None = None
    placeholder: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"
    accept_new_options: bool = False

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "options",
            "default",
            "help",
            "max_selections",
            "placeholder",
            "label_visibility",
            "accept_new_options",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.multiselect(**self.get_kwarg_values())
        return st.multiselect(**self.get_kwarg_values(), key=key + self.label)


class TextArea(FormField, tag="TextArea", tag_field="kind"):
    value: str | None = None
    height: int | None = None
    max_chars: int | None = None
    help: str | None = None
    placeholder: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "value",
            "height",
            "max_chars",
            "help",
            "placeholder",
            "label_visibility",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.text_area(**self.get_kwarg_values())
        return st.text_area(**self.get_kwarg_values(), key=key + self.label)


class NumberInput(FormField):
    format: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"
    icon: str | None = None
    help: str | None = None

    @property
    @abstractmethod
    def kwarg_names(self) -> list[str]: ...

    @abstractmethod
    def display(self, key: str | None = None) -> Any: ...


class NumberInputInt(NumberInput, tag="NumberInputInt", tag_field="kind"):
    min_value: int | None = None
    max_value: int | None = None
    value: int | None = None
    step: int | None = None
    placeholder: int | None = None

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "min_value",
            "max_value",
            "value",
            "step",
            "format",
            "help",
            "placeholder",
            "label_visibility",
            "icon",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.number_input(**self.get_kwarg_values())
        return st.number_input(**self.get_kwarg_values(), key=key + self.label)


class NumberInputFloat(FormField, tag="NumberInputFloat", tag_field="kind"):
    min_value: float | None = None
    max_value: float | None = None
    value: float | None = None
    step: float | None = None
    format: str | None = None
    help: str | None = None
    placeholder: float | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"
    icon: str | None = None

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "min_value",
            "max_value",
            "value",
            "step",
            "format",
            "help",
            "placeholder",
            "label_visibility",
            "icon",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.number_input(**self.get_kwarg_values())
        return st.number_input(**self.get_kwarg_values(), key=key + self.label)


class Checkbox(FormField, tag="Checkbox", tag_field="kind"):
    value: bool = False
    help: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"

    @property
    def kwarg_names(self) -> list[str]:
        return ["label", "value", "help", "label_visibility"]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.checkbox(**self.get_kwarg_values())
        return st.checkbox(**self.get_kwarg_values(), key=key + self.label)


class Radio(FormField, tag="Radio", tag_field="kind"):
    options: list[str]
    index: int | None = 0
    help: str | None = None
    horizontal: bool = False
    captions: list[str] | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "options",
            "index",
            "help",
            "horizontal",
            "captions",
            "label_visibility",
        ]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.radio(**self.get_kwarg_values())
        return st.radio(**self.get_kwarg_values(), key=key + self.label)


class DateInput(FormField, tag="DateInput", tag_field="kind"):
    value: datetime.date | None = None
    min_value: datetime.date | None = None
    max_value: datetime.date | None = None
    help: str | None = None
    format: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"

    @property
    def kwarg_names(self) -> list[str]:
        return [
            "label",
            "value",
            "min_value",
            "max_value",
            "help",
            "format",
            "label_visibility",
        ]

    def display(
        self, key: str | None = None
    ) -> datetime.date | tuple[()] | tuple[datetime.date] | tuple[datetime.date, datetime.date] | None:
        if key is None:
            return st.date_input(**self.get_kwarg_values())
        return st.date_input(**self.get_kwarg_values(), key=key + self.label)


class TimeInput(FormField, tag="TimeInput", tag_field="kind"):
    value: datetime.time | None = None
    help: str | None = None
    step: int | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"

    @property
    def kwarg_names(self) -> list[str]:
        return ["label", "value", "step", "help", "label_visibility"]

    def display(self, key: str | None = None) -> Any:
        if key is None:
            return st.time_input(**self.get_kwarg_values())
        return st.time_input(**self.get_kwarg_values(), key=key + self.label)


class FileUpload(FormField, tag="FileUpload", tag_field="kind"):
    type: list[str] | None = None
    accept_multiple_files: bool = False
    help: str | None = None
    label_visibility: Literal["visible", "hidden", "collapsed"] = "visible"

    @property
    def kwarg_names(self) -> list[str]:
        return ["label", "type", "accept_multiple_files", "help", "label_visibility"]

    def display(self, key: str | None = None) -> UploadedFile | list[UploadedFile] | None:
        if key is None:
            return st.file_uploader(**self.get_kwarg_values())
        return st.file_uploader(**self.get_kwarg_values(), key=key + self.label)


class TextField(FormField, tag="TextField", tag_field="kind"):
    text: str

    @property
    def kwarg_names(self) -> list[str]:
        return []

    def display(self, key: str | None = None) -> Any:  # noqa: ARG002
        st.markdown(self.text)


class CompositeFormField(FormField, tag="CompositeFormField", tag_field="kind"):
    @property
    @abstractmethod
    def kwarg_names(self) -> list[str]: ...

    @abstractmethod
    def display(self, key: str | None = None) -> Any: ...


class OneRequired(CompositeFormField, tag="OneRequired", tag_field="kind"):
    fields: list[Checkbox]

    @property
    def kwarg_names(self) -> list[str]:
        return []

    def display(self, key: str | None = None) -> dict[str, Any]:
        st.markdown(self.label)
        return {field.label: field.display(key) for field in self.fields}


class OnSelection(CompositeFormField, tag="OnSelection", tag_field="kind"):
    selection_field: Radio | Selectbox
    mapping: dict[
        str,
        list[
            TextInput
            | Selectbox
            | Multiselect
            | TextArea
            | NumberInputInt
            | NumberInputFloat
            | Checkbox
            | Radio
            | DateInput
            | TimeInput
            | FileUpload
            | TextField
        ],
    ]

    @property
    def kwarg_names(self) -> list[str]:
        return []

    def display(self, key: str | None = None) -> dict[str, Any]:  # noqa: ARG002
        st.markdown(self.label)
        value = self.selection_field.display()
        data = {self.selection_field.label: value}
        if "__ANY__" in self.mapping:
            fields = self.mapping["__ANY__"]
        elif value in self.mapping:
            fields = self.mapping[value]
        else:
            fields = []

        for field in fields:
            data[field.label] = field.display()
        return data


class DynamicSection(CompositeFormField, tag="DynamicSection", tag_field="kind"):
    fields: list[
        TextInput
        | Selectbox
        | Multiselect
        | TextArea
        | NumberInputInt
        | NumberInputFloat
        | Checkbox
        | Radio
        | DateInput
        | TimeInput
        | FileUpload
        | TextField
    ]
    default_amount: int

    @property
    def kwarg_names(self) -> list[str]:
        return []

    def display(self, key: str | None = None) -> list[dict[str, Any]]:
        translation = st.session_state.translations[st.session_state.language]["forms"]
        st.markdown(self.label)
        amount = (
            st.number_input(translation["amount"], min_value=0, value=self.default_amount, step=1)
            if key is None
            else st.number_input(translation["amount"], min_value=0, value=self.default_amount, step=1, key=key + self.label)
        )
        data: list[dict[str, Any]] = []
        for i in range(amount):
            values: dict[str, Any] = {}
            with st.container(border=True):
                st.markdown(f"{self.label} {i + 1}")
                for field in sorted(self.fields, key=lambda x: x.position):
                    values[field.label] = field.display(key=f"{key}_{self.label}_{i}")
            if len(values) > 0:
                data.append(values)
        return data


class FormStep(Step, tag="FormStep", tag_field="kind"):
    fields: list[
        TextInput
        | Selectbox
        | Multiselect
        | TextArea
        | NumberInputInt
        | NumberInputFloat
        | Checkbox
        | Radio
        | DateInput
        | TimeInput
        | FileUpload
        | TextField
        | OneRequired
        | OnSelection
        | DynamicSection
    ]
    clear_on_submit: bool = False
    border: bool = True
    on_click: Callable[[FormData, str, str], None] | None = None

    def subscribe_on_form_submit(self, callback: Callable[[FormData, str, str], None]) -> None:
        self.on_click = callback

    def display(self) -> None:
        super().display()
        data = FormData(fields={}, attachments=[])
        submit_disabled = False
        for field in sorted(self.fields, key=lambda x: x.position):
            if isinstance(field, TextField):
                field.display()
                continue
            if isinstance(field, OneRequired):
                if not any((values := field.display()).values()):
                    submit_disabled = True
                else:
                    data.fields |= values
            elif isinstance(field, OnSelection):
                data.fields |= field.display()
            elif isinstance(field, FileUpload):
                attachments: UploadedFile | list[UploadedFile] | None
                if (attachments := field.display()) is not None:
                    attachments = [attachments] if isinstance(attachments, UploadedFile) else attachments
                    for attachment in attachments:
                        data.attachments.append(
                            ImageAttachment(
                                name=attachment.name,
                                content=base64.b64encode(attachment.getvalue()).decode("utf-8"),
                                type=attachment.type,
                            )
                        )
            else:
                data.fields[field.label] = field.display()
        translation = st.session_state.translations[st.session_state.language]["forms"]
        if st.button(translation["submit"], disabled=submit_disabled) and self.on_click is not None:
            username = f"{st.user.name} <{st.user.email}>"
            with st.spinner():
                self.on_click(data, username, self.title)
            st.success(translation["submitted"])


class NamedNetboxAsset(msgspec.Struct):
    name: str


class NetboxSearchResult(msgspec.Struct):
    id: int | str
    created: datetime.datetime | None
    last_updated: datetime.datetime | None
    description: str
    comments: str
    url: str

    def _display(self, identifier: str, fields: list[str]) -> None:
        translation = st.session_state.translations[st.session_state.language]["netbox"]
        with st.container(border=True):
            st.markdown(f"ID: {self.id}")
            st.markdown(identifier)
            with st.expander(translation["details"]):
                for field in fields:
                    st.markdown(field)
            st.link_button(translation["view"], self.url)


class NetboxIPSearchResult(NetboxSearchResult, tag="NetboxIPSearchResult", tag_field="kind"):
    address: str
    dns_name: str
    tenant: NamedNetboxAsset | None

    def display(self) -> None:
        translation = st.session_state.translations[st.session_state.language]["netbox"]["ip_address"]
        super()._display(
            translation["identifier"] + self.address,
            [
                translation["created"] + str(self.created),
                translation["last_updated"] + str(self.last_updated),
                translation["description"] + self.description,
                translation["comments"] + self.comments,
                translation["dns"] + self.dns_name,
                translation["tenant"] + ('' if self.tenant is None else self.tenant.name),
            ],
        )


class NetboxPrefixSearchResult(NetboxSearchResult, tag="NetboxPrefixSearchResult", tag_field="kind"):
    prefix: str
    tenant: NamedNetboxAsset | None

    def display(self) -> None:
        translation = st.session_state.translations[st.session_state.language]["netbox"]["prefix"]
        super()._display(
            translation["identifier"] + self.prefix,
            [
                translation["created"] + str(self.created),
                translation["last_updated"] + str(self.last_updated),
                translation["description"] + self.description,
                translation["comments"] + self.comments,
                translation["tenant"] + ('' if self.tenant is None else self.tenant.name),
            ],
        )


class NetboxTenantSearchResult(NetboxSearchResult, tag="NetboxTenantSearchResult", tag_field="kind"):
    name: str
    group: NamedNetboxAsset | None

    def display(self) -> None:
        translation = st.session_state.translations[st.session_state.language]["netbox"]["tenant"]
        super()._display(
            translation["identifier"] + self.name,
            [
                translation["created"] + str(self.created),
                translation["last_updated"] + str(self.last_updated),
                translation["description"] + self.description,
                translation["comments"] + self.comments,
                translation["group"] + ('' if self.group is None else self.group.name),
            ],
        )


class NetboxSearchStep(Step, tag="NetboxSearchStep", tag_field="kind"):
    query_type: NetboxQueryType
    on_click: (
        Callable[[str, str], list[NetboxIPSearchResult | NetboxPrefixSearchResult | NetboxTenantSearchResult]] | None
    ) = None

    def subscribe_on_search(
        self,
        callback: Callable[
            [str, str], list[NetboxIPSearchResult | NetboxPrefixSearchResult | NetboxTenantSearchResult]
        ],
    ) -> None:
        self.on_click = callback

    def display(self) -> None:
        translation = st.session_state.translations[st.session_state.language]["search"]
        super().display()
        query = st.text_input(translation["search_by"], key=f"query_{self.id}")
        fst, snd, _ = st.columns([1, 1, 5])
        if fst.button(translation["search"]) and self.on_click is not None:
            with st.spinner():
                data = self.on_click(query, self.query_type.value.lower())
            snd.button(translation["reset"], on_click=lambda: st.session_state.__setitem__(f"query_{self.id}", ""))
            if not data:
                translation = st.session_state.translations[st.session_state.language]
                st.markdown(translation["search"]["not_found"])
                return
            for item in data:
                item.display()


class OutlineDocument(msgspec.Struct):
    id: str
    title: str
    text: str
    url: str
    url_id: str = msgspec.field(name="urlId")
    collection_id: str = msgspec.field(name="collectionId")
    created_at: datetime.datetime = msgspec.field(name="createdAt")

    def display(self) -> None:
        translation = st.session_state.translations[st.session_state.language]["outline"]
        with st.container(border=True):
            st.markdown(f"ID: {self.id}")
            st.markdown(translation["title"] + self.title)
            with st.expander(translation["details"]):
                st.markdown(translation["created"] + str(self.created_at))
                st.markdown(translation["contents"] + self.text)
            st.link_button(translation["view"], self.url)


class OutlineResult(msgspec.Struct):
    document: OutlineDocument
    context: str
    ranking: float


class OutlineSearchStep(Step, tag="OutlineSearchStep", tag_field="kind"):
    on_click: Callable[[str], list[OutlineResult]] | None = None

    def subscribe_on_search(self, callback: Callable[[str], list[OutlineResult]] | None) -> None:
        self.on_click = callback

    def display(self) -> None:
        translation = st.session_state.translations[st.session_state.language]["search"]
        super().display()
        query = st.text_input(translation["search_by"], key=f"query_{self.id}")
        fst, snd, _ = st.columns([1, 1, 5])
        if fst.button(translation["search"]) and self.on_click is not None:
            with st.spinner():
                data = self.on_click(query)
            snd.button(translation["reset"], on_click=lambda: st.session_state.__setitem__("query_{self.id}", ""))
            if not data:
                st.markdown(translation["not_found"])
                return
            for item in data:
                item.document.display()
