# -*- coding: utf-8 -*-
import re

from datetime import datetime
from django.conf import settings
from email.utils import getaddresses
from ninja import Schema
from pydantic import HttpUrl, Field, MissingError, SetMaxLengthError, validator, constr
from pydantic.generics import GenericModel
from typing import Optional, List, Dict, Literal, Union, Any, TypeVar, Generic, Annotated
from urllib.parse import parse_qs, urlparse

from events.surveyme_integration.filters import FilterTypes
from events.surveyme_integration.variables import (
    VariableTypes, VariableCategoryTypes,
    RendererTypes, renderer_to_variable,
)
from events.v3.types import (
    ConditionItemType,
    ConditionType,
    ErrorType,
    HttpMethodType,
    LanguageType,
    LayerType,
    NotificationFieldType,
    NotificationType,
    OperatorType,
    PostPutFormatType,
    ResultType,
    SubscriptionType,
    SurveyId,
    TrackerIssue,
    TrackerQueue,
    VariableId,
    OperationStatus,
    UnsubscribeStatusType,
    PermissionAccessType,
    PermissionActionType,
)


T = TypeVar('T', bound='Schema')


class ConHttpUrl(HttpUrl):
    max_length = 255


class LinksOut(Schema):
    next: Optional[str]


class PaginationOut(GenericModel, Generic[T]):
    links: LinksOut
    result: List[T]

    @classmethod
    def get_item_class(cls):
        return T


def set_tvm_client(tvm_client: str) -> str:
    if settings.IS_BUSINESS_SITE:
        return None
    return tvm_client


class ErrorOut(Schema):
    loc: List[Any]
    error_code: ErrorType
    msg: str
    value: Optional[Any]


class ConditionItemIn(Schema):
    operator: OperatorType  # required
    type: ConditionItemType  # required
    condition: ConditionType  # required
    question: Optional[str]
    value: Optional[constr(max_length=100)]


class ConditionItemOut(ConditionItemIn):
    pass


class ConditionIn(Schema):
    operator: Optional[str]  # not used yet
    items: Optional[List[ConditionItemIn]]


class ConditionOut(Schema):
    id: int
    operator: str
    items: Optional[List[ConditionItemOut]]


class QuestionsIn(Schema):
    all: Optional[bool]
    items: Optional[List[str]]


class QuestionsOut(Schema):
    all: Optional[bool] = False
    items: Optional[List[str]]


class VariableIn(Schema):
    id: VariableId  # required
    type: VariableTypes  # required
    renderer: Optional[RendererTypes]
    filters: Optional[List[FilterTypes]]
    only_with_value: Optional[bool]
    show_filenames: Optional[bool]
    question: Optional[str]
    questions: Optional[QuestionsIn]
    name: Optional[str]

    @classmethod
    def is_require_question(cls, type_):
        return type_ and (
            type_.name.startswith('form.question_')
            or type_.name in (
                'directory.vip', 'dir_staff.meta_question', 'quiz.question_scores',
                'staff.meta_question', 'staff.external_login',
            )
        )

    @classmethod
    def is_require_questions(cls, type_):
        return type_ and type_.name.startswith('form.questions_')

    @classmethod
    def is_require_name(cls, type_):
        return type_ and type_.name in ('request.header', 'request.query_param', 'request.cookie')

    @validator('question', always=True)
    def validate_question(cls, v, values, **kwargs):
        if cls.is_require_question(values.get('type')):
            if not v:
                raise MissingError
        return v

    @validator('questions', always=True)
    def validate_questions(cls, v, values, **kwargs):
        if cls.is_require_questions(values.get('type')):
            if not v:
                raise MissingError
        return v

    @validator('name', always=True)
    def validate_name(cls, v, values, **kwargs):
        if cls.is_require_name(values.get('type')):
            if not v:
                raise MissingError
        return v

    @validator('renderer')
    def validate_renderer(cls, v, values, **kwargs):
        types_ = renderer_to_variable.get(v)
        if types_:
            if values.get('type') not in types_:
                raise ValueError
        return v


class VariableOut(Schema):
    id: VariableId
    type: VariableTypes
    name: Optional[str]
    renderer: Optional[RendererTypes]
    filters: Optional[List[FilterTypes]]
    only_with_value: Optional[bool]
    show_filenames: Optional[bool]
    question: Optional[str]
    questions: Optional[QuestionsOut]


class VariableCategoryInfoOut(Schema):
    type: VariableCategoryTypes
    name: Optional[str]


class RendererInfoOut(Schema):
    type: RendererTypes
    name: Optional[str]


class VariableInfoOut(Schema):
    type: VariableTypes
    name: Optional[str]
    category: Optional[VariableCategoryInfoOut]
    filters: Optional[dict]
    arguments: Optional[list]
    renderers: Optional[List[RendererInfoOut]]
    connect_only: Optional[bool]


class VariablesOut(Schema):
    subscriptions: Dict[int, List[VariableOut]]
    templates: Dict[int, List[VariableOut]]

    def get_subscription(self, subscription_id) -> Dict[int, List[VariableOut]]:
        return self.subscriptions.get(subscription_id)

    def get_template(self, template_id) -> Dict[int, List[VariableOut]]:
        return self.templates.get(template_id)


class HeaderIn(Schema):
    name: constr(max_length=255)  # required
    value: Optional[constr(max_length=255)]
    only_with_value: Optional[bool] = False


class HeaderOut(Schema):
    name: str
    value: Optional[str]
    only_with_value: bool = False


class EmailHeaderIn(HeaderIn):
    @validator('name')
    def validate_name(cls, v):
        if not cls._is_header_valid(v):
            raise ValueError('incorrent value')
        return v

    @classmethod
    def _is_header_valid(cls, header):
        if settings.IS_BUSINESS_SITE:
            lower_header = header.lower()
            return lower_header == 'reply-to' or lower_header.startswith('x-')
        return True


ParamIn = HeaderIn
ParamOut = HeaderOut


class TrackerFieldKeyIn(Schema):
    name: Optional[str]
    slug: str  # required
    type: str = 'string'  # required


class TrackerFieldKeyOut(Schema):
    name: Optional[str]
    slug: str
    type: str = 'string'


class TrackerFieldIn(Schema):
    key: Union[str, TrackerFieldKeyIn]  # required
    value: Optional[str]
    only_with_value: Optional[bool] = False


class TrackerFieldOut(Schema):
    key: TrackerFieldKeyOut
    value: Optional[str]
    only_with_value: bool = False


class QuestionAttachmentsIn(Schema):
    all: Optional[bool]
    items: Optional[List[str]]


class QuestionAttachmentsOut(Schema):
    all: bool
    items: Optional[List[str]]


class StaticAttachmentIn(Schema):
    path: str  # constr(max_length=100)  # required

    @validator('path')
    def set_path(cls, url):
        result = url
        if url.startswith('http'):
            paths = parse_qs(urlparse(url).query).get('path')
            if paths:
                result = paths[0]
        if len(result) > 100:
            raise SetMaxLengthError(limit_value=100)
        return result


class StaticFile:
    def __init__(self, attach: StaticAttachmentIn):
        self.file = attach.path
        self._committed = True

    def __str__(self):
        return self.file


class StaticAttachmentOut(Schema):
    name: Optional[str]
    links: Optional[Dict[str, str]]


class TemplateAttachmentOut(Schema):
    name: str
    body: Optional[str]
    mime: Optional[str]
    variables: Optional[List[VariableOut]]


class AttachmentsIn(Schema):
    question: Optional[QuestionAttachmentsIn]
    static: Optional[List[StaticAttachmentIn]]


class AttachmentsOut(Schema):
    question: Optional[QuestionAttachmentsOut]
    static: Optional[List[StaticAttachmentOut]]
    template: Optional[List[TemplateAttachmentOut]]


class BaseSubscriptionIn(Schema):
    active: Optional[bool]
    follow: Optional[bool]
    language: Optional[LanguageType]
    variables: Optional[List[VariableIn]]


class BaseSubscriptionOut(Schema):
    id: int
    active: bool = False
    follow: bool = False
    language: LanguageType
    variables: Optional[List[VariableOut]]


class TrackerIn(BaseSubscriptionIn):
    type: Literal[SubscriptionType.tracker]
    issue_type: Optional[int]  # required for create
    priority: Optional[int]  # required for create
    queue: Optional[TrackerQueue]  # required for create
    parent: Optional[TrackerIssue]  # required for create
    author: Optional[constr(max_length=255)]
    assignee: Optional[constr(max_length=255)]
    subject: Optional[constr(max_length=255)]
    body: Optional[str]
    fields: Optional[List[TrackerFieldIn]]
    attachments: Optional[AttachmentsIn]

    @validator('issue_type')
    def validate_issue_type(cls, v):
        if v < 0:
            raise ValueError('incorrect value')
        return v

    @validator('priority')
    def validate_priority(cls, v):
        if v < 0:
            raise ValueError('incorrect value')
        return v

    @validator('queue')
    def set_queue_upper(cls, v):
        if not v:
            return v
        return v.upper()

    @validator('parent')
    def set_parent_upper(cls, v):
        if not v:
            return v
        return v.upper()


class TrackerExtraOut(Schema):
    queue: Optional[str]
    parent: Optional[str]
    author: Optional[str]
    assignee: Optional[str]
    issue_type: Optional[int]
    priority: Optional[int]
    fields: Optional[List[TrackerFieldOut]]


class TrackerOut(BaseSubscriptionOut, TrackerExtraOut):
    type: Literal[SubscriptionType.tracker]
    subject: Optional[str]
    body: Optional[str]
    attachments: Optional[AttachmentsOut]


class WikiIn(BaseSubscriptionIn):
    type: Literal[SubscriptionType.wiki]
    supertag: Optional[constr(max_length=255)]  # required for create
    body: Optional[str]

    @validator('supertag')
    def set_supertag(cls, v):
        p = urlparse(v)
        supertag = p.path.strip('/')
        if p.fragment:
            supertag += f'#{p.fragment}'
        return supertag


class WikiExtraOut(Schema):
    supertag: Optional[str]
    body: Optional[str]


class WikiOut(BaseSubscriptionOut, WikiExtraOut):
    type: Literal[SubscriptionType.wiki]


class EmailIn(BaseSubscriptionIn):
    type: Literal[SubscriptionType.email]
    subject: Optional[constr(max_length=255)]
    body: Optional[str]
    email_to_address: Optional[constr(max_length=255)]  # required for create
    email_from_title: Optional[constr(max_length=255)]
    email_from_address: Optional[constr(max_length=255)]
    email_spam_check: Optional[bool]
    headers: Optional[List[EmailHeaderIn]]
    attachments: Optional[AttachmentsIn]

    @validator('email_to_address')
    def validate_email_to_address(cls, v):
        variable_re = re.compile(r'^\{[0-9a-fA-F]{24}\}$')
        parts = getaddresses((v, ))
        emails = []
        for (_, email) in parts:
            if not email:
                continue
            if not variable_re.match(email) and '@' not in email:
                raise ValueError('incorrent value')
            emails.append(email)
        if not emails:
            raise ValueError('incorrent value')
        return v


class EmailOut(BaseSubscriptionOut):
    type: Literal[SubscriptionType.email]
    subject: Optional[str]
    body: Optional[str]
    email_to_address: Optional[str]
    email_from_title: Optional[str]
    email_from_address: Optional[str]
    email_spam_check: Optional[bool]
    headers: Optional[List[HeaderOut]]
    attachments: Optional[AttachmentsOut]


class JsonRpcIn(BaseSubscriptionIn):
    type: Literal[SubscriptionType.jsonrpc]
    url: Optional[ConHttpUrl]  # required for create
    method: Optional[constr(max_length=255)]
    tvm_client: Optional[constr(max_length=25)]
    params: Optional[List[ParamIn]]
    headers: Optional[List[HeaderIn]]

    _set_tvm_client = validator('tvm_client', allow_reuse=True)(set_tvm_client)


class JsonRpcExtraOut(Schema):
    method: Optional[str]


class JsonRpcOut(BaseSubscriptionOut, JsonRpcExtraOut):
    type: Literal[SubscriptionType.jsonrpc]
    url: Optional[str]
    tvm_client: Optional[str]
    params: Optional[List[ParamOut]]
    headers: Optional[List[HeaderOut]]


class BasePostPutIn(BaseSubscriptionIn):
    url: Optional[ConHttpUrl]  # required for create
    format: Optional[PostPutFormatType]
    tvm_client: Optional[constr(max_length=25)]
    questions: Optional[QuestionsIn]
    headers: Optional[List[HeaderIn]]

    _set_tvm_client = validator('tvm_client', allow_reuse=True)(set_tvm_client)


class PostIn(BasePostPutIn):
    type: Literal[SubscriptionType.post]


class PutIn(BasePostPutIn):
    type: Literal[SubscriptionType.put]


class BasePostPutOut(BaseSubscriptionOut):
    url: Optional[str]
    format: Optional[PostPutFormatType]
    tvm_client: Optional[str]
    questions: Optional[QuestionsOut]
    headers: Optional[List[HeaderOut]]


class PostOut(BasePostPutOut):
    type: Literal[SubscriptionType.post]


class PutOut(BasePostPutOut):
    type: Literal[SubscriptionType.put]


class HttpIn(BaseSubscriptionIn):
    type: Literal[SubscriptionType.http]
    url: Optional[ConHttpUrl]  # required for create
    method: Optional[HttpMethodType]  # required for create
    body: Optional[str]
    tvm_client: Optional[constr(max_length=25)]
    headers: Optional[List[HeaderOut]]

    _set_tvm_client = validator('tvm_client', allow_reuse=True)(set_tvm_client)


class HttpOut(BaseSubscriptionOut):
    type: Literal[SubscriptionType.http]
    url: Optional[str]
    method: Optional[HttpMethodType]
    body: Optional[str]
    tvm_client: Optional[str]
    headers: Optional[List[HeaderOut]]


class SubscriptionIn(Schema):
    __root__: Union[EmailIn, TrackerIn, WikiIn, JsonRpcIn, PostIn, PutIn, HttpIn] = \
        Field(discriminator='type')


class SubscriptionOut(Schema):
    __root__: Union[EmailOut, TrackerOut, WikiOut, JsonRpcOut, PostOut, PutOut, HttpOut]


class HookIn(Schema):
    name: Optional[constr(max_length=100)]
    active: Optional[bool]


class HookOut(Schema):
    id: int
    name: Optional[str]
    active: bool = False
    conditions: Optional[List[ConditionOut]]
    subscriptions: Optional[List[SubscriptionOut]]


class SurveyOut(Schema):
    id: SurveyId
    name: Optional[str]
    dir_id: Optional[str]
    is_published: Optional[bool]
    is_public: Optional[bool]
    language: Optional[str]
    hooks: Optional[List[HookOut]]


class ResultOkOut(Schema):
    status: Literal[ResultType.ok]


class ResultSkipOut(Schema):
    status: Literal[ResultType.skip]


class ResultFailOut(Schema):
    status: Literal[ResultType.fail]
    detail: Optional[str]


class ResultOperationOut(Schema):
    status: Literal[ResultType.operation]
    operation_id: str


ResultOut = Annotated[
    Union[ResultOkOut, ResultSkipOut, ResultFailOut, ResultOperationOut],
    Field(discriminator='status')
]


class NotificationOut(Schema):
    id: int
    status: NotificationType
    created: datetime
    finished: Optional[datetime]
    survey_id: Optional[SurveyId]
    survey_name: Optional[str]
    hook_id: Optional[int]
    hook_name: Optional[str]
    subscription_id: Optional[int]
    answer_id: Optional[int]
    user_id: Optional[int]
    type: Optional[SubscriptionType]
    comment: Optional[str]


class NotificationFieldOut(Schema):
    name: str
    value: str
    type: NotificationFieldType = NotificationFieldType.text


class DetailedNotificationOut(NotificationOut):
    context: Optional[List[NotificationFieldOut]]
    response: Optional[List[NotificationFieldOut]]
    error: Optional[List[NotificationFieldOut]]


ListNotificationOut = PaginationOut[NotificationOut]


class NotificationsIn(Schema):
    ids: List[int]


class RestartNotificationOut(Schema):
    id: int
    survey_id: SurveyId
    subscription_id: int
    result: ResultOut


class StatusNotificationOut(Schema):
    id: int
    status: NotificationType


class UserOut(Schema):
    id: int
    uid: Optional[str]
    cloud_uid: Optional[str]
    login: Optional[str]
    display: Optional[str]
    email: Optional[str]
    is_superuser: Optional[bool]
    is_staff: Optional[bool]


class UserSettingsSchemaIn(Schema):
    settings: Dict[Any, Any]


class UserSettingsSchemaOut(Schema):
    id: int
    settings: Dict[Any, Any]


class OperationsInfoIn(Schema):
    ids: List[str]


class OperationInfoOut(Schema):
    id: str
    status: OperationStatus
    message: Optional[str]


class SurveySuggestOut(Schema):
    layer: Literal[LayerType.survey]
    id: SurveyId
    name: Optional[str]


class HookSuggestOut(Schema):
    layer: Literal[LayerType.hook]
    id: int
    name: Optional[str]
    survey_id: SurveyId
    survey_name: Optional[str]


class SubscriptionSuggestOut(Schema):
    layer: Literal[LayerType.subscription]
    id: int
    type: SubscriptionType
    hook_id: int
    hook_name: Optional[str]
    survey_id: SurveyId
    survey_name: Optional[str]


SuggestOut = Annotated[
    Union[SurveySuggestOut, HookSuggestOut, SubscriptionSuggestOut],
    Field(discriminator='layer')
]


class UnsubscribeSchemaOut(Schema):
    status: UnsubscribeStatusType
    survey_name: Optional[str]


class SurveyGroupOut(Schema):
    id: int
    name: Optional[str]
    dir_id: Optional[str]


class UserIn(Schema):
    uid: Optional[str]
    cloud_uid: Optional[str]


class GroupOut(Schema):
    id: int
    name: Optional[str]
    url: Optional[str]


class PermissionIn(Schema):
    action: PermissionActionType = PermissionActionType.change
    users: Optional[List[Union[str, UserIn]]]
    groups: Optional[List[int]]
    access: PermissionAccessType = PermissionAccessType.owner

    @validator('access')
    def set_access(cls, v, values, **kwargs):
        if v == PermissionAccessType.restricted:
            if not values.get('users') and not values.get('groups'):
                v = PermissionAccessType.owner
        return v


class PermissionOut(Schema):
    access: PermissionAccessType
    action: PermissionActionType
    users: Optional[List[UserOut]]
    groups: Optional[List[GroupOut]]
