from marshmallow import (
    ValidationError,
    fields,
    post_load,
    pre_dump,
    validate,
    validates_schema,
)

from maps_adv.common.protomallow import PbEnumField, ProtobufSchema, with_schemas
from maps_adv.geosmb.harmonist.proto import pipeline_pb2
from maps_adv.geosmb.harmonist.server.lib.domain import Domain
from maps_adv.geosmb.harmonist.server.lib.enums import (
    ColumnType,
    FileExtension,
    PipelineStep,
    StepStatus,
)

ENUMS_MAP = {
    "column_type": [
        (pipeline_pb2.ColumnTypeMap.ColumnType.DO_NOT_IMPORT, ColumnType.DO_NOT_IMPORT),
        (pipeline_pb2.ColumnTypeMap.ColumnType.FIRST_NAME, ColumnType.FIRST_NAME),
        (pipeline_pb2.ColumnTypeMap.ColumnType.LAST_NAME, ColumnType.LAST_NAME),
        (pipeline_pb2.ColumnTypeMap.ColumnType.PHONE, ColumnType.PHONE),
        (pipeline_pb2.ColumnTypeMap.ColumnType.EMAIL, ColumnType.EMAIL),
        (pipeline_pb2.ColumnTypeMap.ColumnType.COMMENT, ColumnType.COMMENT),
    ],
    "file_extension": [
        (pipeline_pb2.File.Extension.CSV, FileExtension.CSV),
        (pipeline_pb2.File.Extension.XLSX, FileExtension.XLSX),
    ],
    "pipeline_step": [
        (pipeline_pb2.PipelineStep.IMPORTING_CLIENTS, PipelineStep.IMPORTING_CLIENTS),
        (pipeline_pb2.PipelineStep.VALIDATING_DATA, PipelineStep.VALIDATING_DATA),
        (pipeline_pb2.PipelineStep.PARSING_DATA, PipelineStep.PARSING_DATA),
    ],
    "pipeline_step_status": [
        (pipeline_pb2.PipelineStepStatus.IN_PROGRESS, StepStatus.IN_PROGRESS),
        (pipeline_pb2.PipelineStepStatus.FINISHED, StepStatus.FINISHED),
        (pipeline_pb2.PipelineStepStatus.FAILED, StepStatus.FAILED),
    ],
}


class ColumnTypeMapSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.ColumnTypeMap

    column_type = PbEnumField(
        enum=ColumnType,
        pb_enum=pipeline_pb2.ColumnTypeMap.ColumnType,
        values_map=ENUMS_MAP["column_type"],
        required=True,
    )
    column_number = fields.Integer(required=True)


class MarkUpSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.MarkUp

    ignore_first_line = fields.Bool(required=True)
    column_type_map = fields.Nested(
        ColumnTypeMapSchema, many=True, validate=validate.Length(min=1)
    )
    segment = fields.String(validate=validate.Length(min=1))

    @post_load
    def _validate_markup(self, data: dict) -> None:
        column_type_map = data["column_type_map"]

        column_numbers = [item["column_number"] for item in column_type_map]
        if len(set(column_numbers)) != len(column_numbers):
            raise ValidationError("Column numbers must be unique.")

        column_types = [
            item["column_type"]
            for item in column_type_map
            if item["column_type"] != ColumnType.DO_NOT_IMPORT
        ]

        if not column_types:
            raise ValidationError("Specify at least one column type.")
        if len(set(column_types)) != len(column_types):
            raise ValidationError("Column types must be unique.")
        if (
            ColumnType.EMAIL not in column_types
            and ColumnType.PHONE not in column_types
        ):
            raise ValidationError("Email or phone must be set.")


class RowSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.Row

    cells = fields.List(fields.String())

    @pre_dump
    def to_dict(self, data: list) -> dict:
        return {"cells": data}


class PreviewStateSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.PreviewState

    rows = fields.Nested(RowSchema, many=True)
    markup = fields.Nested(MarkUpSchema)


class FileSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.File

    content = fields.Raw(required=True, validate=validate.Length(min=1))
    extension = PbEnumField(
        required=True,
        enum=FileExtension,
        pb_enum=pipeline_pb2.File.Extension,
        values_map=ENUMS_MAP["file_extension"],
    )


class ShowPreviewInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.ShowPreviewInput

    biz_id = fields.Integer(required=True, validate=validate.Range(min=1))
    session_id = fields.String(required=True, validate=validate.Length(min=1))


class SubmitMarkUpSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.SubmitMarkUp

    biz_id = fields.Integer(required=True, validate=validate.Range(min=1))
    session_id = fields.String(required=True, validate=validate.Length(min=1))
    markup = fields.Nested(MarkUpSchema, required=True)


class CreateClientsOutputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.CreateClientsOutput

    session_id = fields.String(required=True)

    @pre_dump
    def to_dict(self, data: str) -> dict:
        return {"session_id": data}


class CreateClientsInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.CreateClientsInput

    biz_id = fields.Integer(required=True, validate=validate.Range(min=1))
    text = fields.String(validate=validate.Length(min=1))
    file = fields.Nested(FileSchema())

    @validates_schema(skip_on_field_errors=True)
    def _validate(self, data: dict) -> None:
        if "file" not in data and "text" not in data:
            raise ValidationError("One of data fields must be set: text, file")

    @post_load
    def _to_flat(self, data: dict) -> dict:
        file_data = data.pop("file", {})

        if file_data:
            data["file_content"] = file_data["content"]
            data["file_extension"] = file_data["extension"]

        return data


class ImportClientsInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.ImportClientsInput

    biz_id = fields.Integer(required=True, validate=validate.Range(min=1))
    session_id = fields.String(required=True, validate=validate.Length(min=1))


class InvalidClientsReportSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.InvalidClientsReport

    invalid_clients_amount = fields.Integer()
    report_link = fields.String()


class MarkUpValidationResultSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.MarkUpValidationResult

    valid_clients_amount = fields.Integer()
    invalid_clients = fields.Nested(InvalidClientsReportSchema)


class ImportClientsResultSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.ImportClientsResult

    new_clients_amount = fields.Integer()
    updated_clients_amount = fields.Integer()


class PipelineStatusSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.PipelineStatus

    step = PbEnumField(
        enum=PipelineStep,
        pb_enum=pipeline_pb2.PipelineStep,
        values_map=ENUMS_MAP["pipeline_step"],
        required=True,
    )
    step_status = PbEnumField(
        enum=StepStatus,
        pb_enum=pipeline_pb2.PipelineStepStatus,
        values_map=ENUMS_MAP["pipeline_step_status"],
        required=True,
    )
    preview = fields.Nested(PreviewStateSchema)
    validation_result = fields.Nested(MarkUpValidationResultSchema)
    import_result = fields.Nested(ImportClientsResultSchema)
    failed_reason = fields.String()


class FetchPipelineStatusSchema(ProtobufSchema):
    class Meta:
        pb_message_class = pipeline_pb2.FetchPipelineStatus

    biz_id = fields.Integer(required=True, validate=validate.Range(min=1))
    session_id = fields.String(required=True, validate=validate.Length(min=1))


class ApiProvider:
    _domain: Domain

    def __init__(self, domain: Domain):
        self._domain = domain

    @with_schemas(
        input_schema=CreateClientsInputSchema, output_schema=CreateClientsOutputSchema
    )
    async def submit_data(self, **kwargs):
        return await self._domain.submit_data(**kwargs)

    @with_schemas(input_schema=ShowPreviewInputSchema, output_schema=PreviewStateSchema)
    async def show_preview(self, **kwargs):
        return await self._domain.show_preview(**kwargs)

    @with_schemas(input_schema=SubmitMarkUpSchema)
    async def submit_markup(self, **kwargs):
        await self._domain.submit_markup(**kwargs)

    @with_schemas(input_schema=ImportClientsInputSchema)
    async def import_clients(self, **kwargs):
        await self._domain.import_clients(**kwargs)

    @with_schemas(
        input_schema=FetchPipelineStatusSchema, output_schema=PipelineStatusSchema
    )
    async def fetch_pipeline_status(self, **kwargs):
        return await self._domain.fetch_pipeline_status(**kwargs)
