import json
from operator import itemgetter

import jsonschema.exceptions
import pytz
from marshmallow import (
    ValidationError,
    fields,
    post_load,
    pre_dump,
    validates,
    validates_schema,
)

from maps_adv.adv_store.api.proto import (
    campaign_list_pb2,
    campaign_pb2,
    campaign_status_pb2,
    discount_pb2,
    order_pb2,
)
from maps_adv.adv_store.api.proto.error_pb2 import Error
from maps_adv.adv_store.api.proto.prediction_pb2 import (
    DailyBudgetPredictionInput,
    DailyBudgetPredictionOutput,
)
from maps_adv.adv_store.api.schemas.enums import (
    CampaignStatusEnum,
    OrderSizeEnum,
    OverviewPositionEnum,
    PlatformEnum,
    PublicationEnvEnum,
    ResolveUriTargetEnum,
    RubricEnum,
)
from maps_adv.common.helpers.enums import CampaignTypeEnum
from maps_adv.common.proto.campaign_pb2 import (
    CampaignType,
)
from maps_adv.common.protomallow import (
    PbDateTimeField,
    PbEnumField,
    PbMapField,
    PbFixedDecimalDictField,
    ProtobufSchema,
)

from .actions import ActionSchema
from .base import StrictSchema
from .billing import BillingSchema
from .creatives import CreativeSchema
from .enums_map import ENUMS_MAP
from .fields import StrDecimalField
from .placing import PlacingSchema

TARGETING_SCHEMA = {
    "$schema": "http://json-schema.org/draft-06/schema#",
    "$ref": "#/definitions/target",
    "definitions": {
        "target": {
            "type": "object",
            "oneOf": [
                {"$ref": "#/definitions/targetContent"},
                {"$ref": "#/definitions/targetArray"},
                {"$ref": "#/definitions/targetAttributes"},
                {"$ref": "#/definitions/targetEmpty"},
            ],
        },
        "targetContent": {
            "required": ["tag", "content"],
            "oneOf": [
                {
                    "properties": {
                        "tag": {"enum": ["age"]},
                        "content": {
                            "oneOf": [
                                {"$ref": "#/definitions/enumAge"},
                                {
                                    "type": "array",
                                    "minItems": 1,
                                    "items": {"$ref": "#/definitions/enumAge"},
                                },
                            ]
                        },
                    }
                },
                {
                    "properties": {
                        "tag": {"enum": ["gender"]},
                        "content": {
                            "oneOf": [
                                {"$ref": "#/definitions/enumGender"},
                                {
                                    "type": "array",
                                    "minItems": 1,
                                    "items": {"$ref": "#/definitions/enumGender"},
                                },
                            ]
                        },
                    }
                },
                {
                    "properties": {
                        "tag": {"enum": ["income"]},
                        "content": {
                            "oneOf": [
                                {"$ref": "#/definitions/enumIncome"},
                                {
                                    "type": "array",
                                    "minItems": 1,
                                    "items": {"$ref": "#/definitions/enumIncome"},
                                },
                            ]
                        },
                    }
                },
            ],
        },
        "targetArray": {
            "required": ["tag", "items"],
            "properties": {
                "tag": {"enum": ["and", "or"]},
                "items": {"type": "array", "items": {"$ref": "#/definitions/target"}},
            },
        },
        "targetAttributes": {
            "required": ["tag", "attributes"],
            "oneOf": [
                {
                    "properties": {
                        "tag": {"enum": ["segment"]},
                        "attributes": {
                            "type": "object",
                            "required": ["id", "keywordId"],
                            "properties": {
                                "id": {"type": "string"},
                                "keywordId": {"type": "string"},
                            },
                        },
                    }
                },
                {
                    "properties": {
                        "tag": {"enum": ["audience"]},
                        "attributes": {
                            "type": "object",
                            "required": ["id"],
                            "properties": {"id": {"type": "string"}},
                        },
                    }
                },
            ],
        },
        "targetEmpty": {
            "type": "object",
            "properties": {},
            "additionalProperties": False,
        },
        "enumAge": {"enum": ["18-", "18-24", "25-34", "35-44", "45-54", "55+"]},
        "enumGender": {"enum": ["male", "female"]},
        "enumIncome": {"enum": ["low", "middle", "high"]},
    },
}


class WeekScheduleItemSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_pb2.WeekScheduleItem

    start = fields.Integer()
    end = fields.Integer()


class DiscountSchema(ProtobufSchema):
    class Meta:
        pb_message_class = discount_pb2.Discount

    start_datetime = PbDateTimeField()
    end_datetime = PbDateTimeField()
    cost_multiplier = StrDecimalField()


class VerificationDataSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_pb2.VerificationData

    platform = fields.String(required=True)
    params = PbMapField()


class SettingsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_pb2.CampaignSettings

    custom_page_id = fields.String(required=False)
    overview_position = PbEnumField(
        enum=OverviewPositionEnum,
        pb_enum=campaign_pb2.OverviewPosition.Enum,
        values_map=ENUMS_MAP["overview_position"],
    )
    forced_product_version_datetime = PbDateTimeField(required=False)
    verification_data = fields.List(fields.Nested(VerificationDataSchema))
    auto_prolongation = fields.Boolean(required=False)

    @pre_dump
    def predump(self, data):
        verification_data = data.get("verification_data")
        if verification_data is not None and not isinstance(verification_data, list):
            data["verification_data"] = [verification_data]


class CampaignDataSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_pb2.CampaignData

    name = fields.String()
    author_id = fields.Integer()
    created_datetime = PbDateTimeField(required=False)
    publication_envs = fields.List(
        PbEnumField(
            enum=PublicationEnvEnum,
            pb_enum=campaign_pb2.PublicationEnv.Enum,
            values_map=ENUMS_MAP["publication_env"],
        )
    )
    campaign_type = PbEnumField(
        enum=CampaignTypeEnum,
        pb_enum=CampaignType.Enum,
        values_map=ENUMS_MAP["campaign_type"],
    )
    status = PbEnumField(
        enum=CampaignStatusEnum,
        pb_enum=campaign_status_pb2.CampaignStatus.Enum,
        values_map=ENUMS_MAP["campaign_status"],
    )
    start_datetime = PbDateTimeField()
    end_datetime = PbDateTimeField()
    timezone = fields.String()
    billing = fields.Nested(BillingSchema)
    placing = fields.Nested(PlacingSchema)
    platforms = fields.List(
        PbEnumField(
            enum=PlatformEnum,
            pb_enum=campaign_pb2.Platform.Enum,
            values_map=ENUMS_MAP["platforms"],
        )
    )
    creatives = fields.Nested(CreativeSchema, many=True)
    actions = fields.Nested(ActionSchema, many=True)

    week_schedule = fields.Nested(WeekScheduleItemSchema, many=True)
    order_id = fields.Integer(required=False, missing=None)
    manul_order_id = fields.Integer(required=False, missing=None)
    comment = fields.String(required=False)
    user_display_limit = fields.Integer(required=False, missing=None)
    user_daily_display_limit = fields.Integer(required=False, missing=None)
    targeting = fields.String(required=False, missing="{}")
    rubric = PbEnumField(
        enum=RubricEnum,
        pb_enum=campaign_pb2.Rubric.Enum,
        values_map=ENUMS_MAP["rubric"],
    )
    order_size = PbEnumField(
        enum=OrderSizeEnum,
        pb_enum=campaign_pb2.OrderSize.Enum,
        values_map=ENUMS_MAP["order_size"],
    )
    discounts = fields.Nested(DiscountSchema, many=True)
    datatesting_expires_at = PbDateTimeField()
    moderation_verdicts = fields.List(fields.Integer(), dump_only=True)
    settings = fields.Nested(SettingsSchema)
    product_id = fields.Integer(required=False)

    @validates("name")
    def validate_name(self, name):
        if not name:
            raise ValidationError(
                "Name must not be empty", pb_error_code=Error.INVALID_CAMPAIGN_NAME
            )

    @validates("targeting")
    def validate_targeting(self, targeting):
        try:
            targeting = json.loads(targeting)
            if targeting:
                jsonschema.validate(targeting, TARGETING_SCHEMA)
        except (json.JSONDecodeError, jsonschema.exceptions.ValidationError):
            raise ValidationError(
                "Invalid targeting field",
                pb_error_code=Error.TARGETING_DOES_NOT_MATCH_SCHEMA,
            )

    @validates("timezone")
    def validate_timezone(self, timezone):
        if timezone not in pytz.all_timezones_set:
            raise ValidationError(
                "Unknown timezone", pb_error_code=Error.INVALID_TIMEZONE_NAME
            )

    @validates("publication_envs")
    def validate_publication_envs(self, publication_envs):
        if not publication_envs:
            raise ValidationError(
                "Empty publication envs", pb_error_code=Error.PUBLICATION_ENVS_ARE_EMPTY
            )

    @validates("platforms")
    def validate_platforms(self, platforms):
        if not platforms:
            raise ValidationError(
                "Empty platforms", pb_error_code=Error.PLATFORMS_ARE_EMPTY
            )

    @validates("billing")
    def validate_billing(self, billing):
        if "cpm" in billing or "cpa" in billing:
            vals = billing.get("cpm") or billing.get("cpa")
            if vals["cost"] <= 0:
                raise ValidationError(
                    "cost must be positive",
                    pb_error_code=Error.MONEY_QUANTITY_NOT_POSITIVE,
                )
            if vals["budget"] is not None and vals["budget"] <= 0:
                raise ValidationError(
                    "budget must be positive",
                    pb_error_code=Error.MONEY_QUANTITY_NOT_POSITIVE,
                )
            if vals["daily_budget"] is not None and vals["daily_budget"] <= 0:
                raise ValidationError(
                    "daily_budget must be positive",
                    pb_error_code=Error.MONEY_QUANTITY_NOT_POSITIVE,
                )
        elif "fix" in billing:
            if billing["fix"]["cost"] <= 0:
                raise ValidationError(
                    "cost must be positive",
                    pb_error_code=Error.MONEY_QUANTITY_NOT_POSITIVE,
                )
        else:
            raise ValidationError(
                "No billing info", pb_error_code=Error.INVALID_BILLING
            )

    @validates("placing")
    def validate_placing(self, placing):
        if "organizations" in placing:
            if not placing["organizations"]["permalinks"]:
                raise ValidationError(
                    "No permalinks", pb_error_code=Error.PLACING_PERMALINKS_ARE_EMPTY
                )
        elif "area" in placing:
            areas = placing["area"]["areas"]
            if not areas:
                raise ValidationError(
                    "No areas", pb_error_code=Error.PLACING_AREAS_ARE_EMPTY
                )
            if not all(area["points"] for area in areas):
                raise ValidationError(
                    "Empty polygon in areas",
                    pb_error_code=Error.PLACING_AREA_POLYGON_IS_EMPTY,
                )
        else:
            raise ValidationError(
                "Invalid placing", pb_error_code=Error.INVALID_PLACING
            )

    @validates("actions")
    def validate_actions(self, actions):
        for action in actions:
            if action["type_"] == "search" and not action["organizations"]:
                raise ValidationError(
                    "No organizations in search action",
                    pb_error_code=Error.ACTION_SEARCH_ORGANIZATIONS_ARE_EMPTY,
                )
            elif (
                action["type_"] == "resolve_uri"
                and action["target"] == ResolveUriTargetEnum.BROWSER
                and action["dialog"] is None
            ):
                raise ValidationError(
                    "No dialog field in resolve_uri action with BROWSER target",
                    pb_error_code=Error.INVALID_INPUT_DATA,
                )

        if sum(int(action["main"]) for action in actions) > 1:
            raise ValidationError(
                "Got more than one main action",
                pb_error_code=Error.MULTIPLE_MAIN_ACTIONS,
            )

    @validates("creatives")
    def validate_creatives(self, creatives):
        for creative in creatives:
            if (
                ("images" in creative or "images_v2" in creative)
                and not creative.get("images", [])
                and not creative.get("images_v2", [])
            ):
                raise ValidationError(
                    "Empty images in creative",
                    pb_error_code=Error.CREATIVE_IMAGES_ARE_EMPTY,
                )

    @validates("week_schedule")
    def validate_week_schedule(self, schedules):
        if not schedules:
            return

        increasing = []
        for sch in sorted(schedules, key=itemgetter("start")):
            increasing.append(sch["start"])
            increasing.append(sch["end"])

        if increasing[0] < 0 or increasing[-1] > 7 * 24 * 60:
            raise ValidationError(
                "Invalid schedule", pb_error_code=Error.INVALID_WEEK_SCHEDULE
            )

        if not all(x < y for x, y in zip(increasing, increasing[1:])):
            raise ValidationError(
                "Invalid schedule", pb_error_code=Error.INVALID_WEEK_SCHEDULE
            )

    @validates_schema
    def validate_at_least_one_order(self, data):
        if not data.get("order_id") and not data.get("manul_order_id"):
            raise ValidationError(
                "No order specified", pb_error_code=Error.NO_ORDERS_SPECIFIED
            )

        if data["start_datetime"] >= data["end_datetime"]:
            raise ValidationError(
                "No order specified",
                pb_error_code=Error.SHOWING_PERIOD_START_LATER_THAN_END,
            )

    @validates_schema
    def validate_specific_settings(self, data):
        if (
            data.get("settings", {}).get("overview_position")
            and data["campaign_type"] != CampaignTypeEnum.OVERVIEW_BANNER
        ):
            raise ValidationError(
                (
                    "overview_position setting can only be used with OVERVIEW_BANNER "
                    "campaign_type"
                ),
                "settings",
            )

    @post_load
    def postload(self, data):
        billing = data.pop("billing")
        for field in ("cpm", "cpa", "fix"):
            if field in billing:
                data[field] = billing[field]
                break

        data["targeting"] = json.loads(data.get("targeting", "{}"))

    @pre_dump
    def predump(self, data):
        data["targeting"] = json.dumps(data["targeting"])

        for field in ("cpm", "cpa", "fix"):
            if field in data:
                data["billing"] = {field: data.pop(field)}
                break


class CampaignSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_pb2.Campaign

    id = fields.Integer()
    data = fields.Nested(CampaignDataSchema)

    @pre_dump
    def process(self, data):
        return {"id": data.pop("id"), "data": data}


class OrderInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = order_pb2.OrdersInput

    order_ids = fields.List(fields.Integer())
    manul_order_ids = fields.List(fields.Integer())


class ShortCampaignSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.ShortCampaign

    id = fields.Integer()
    name = fields.String()
    order_id = fields.Integer(required=False)
    manul_order_id = fields.Integer(required=False)


class ShortCampaignListSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.ShortCampaignList

    campaigns = fields.List(fields.Nested(ShortCampaignSchema))


class CampaignListItem(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignListItem

    id = fields.Integer()
    name = fields.String()
    timezone = fields.String()
    start_datetime = PbDateTimeField()
    end_datetime = PbDateTimeField()
    status = PbEnumField(
        enum=CampaignStatusEnum,
        pb_enum=campaign_pb2.Platform.Enum,
        values_map=ENUMS_MAP["campaign_status"],
    )
    budget = PbFixedDecimalDictField(places=4, field="value", required=False)
    publication_envs = fields.List(
        PbEnumField(
            enum=PublicationEnvEnum,
            pb_enum=campaign_pb2.PublicationEnv.Enum,
            values_map=ENUMS_MAP["publication_env"],
        )
    )
    datatesting_expires_at = PbDateTimeField()


class CampaignListSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignList

    campaigns = fields.List(fields.Nested(CampaignListItem))


class OrderSummarySchema(ProtobufSchema):
    class Meta:
        pb_message_class = order_pb2.OrderSummary

    total = fields.Integer()
    active = fields.Integer()
    order_id = fields.Integer(required=False)
    manul_order_id = fields.Integer(required=False)


class OrderSummaryListSchema(ProtobufSchema):
    class Meta:
        pb_message_class = order_pb2.OrderSummaryList

    orders = fields.List(fields.Nested(OrderSummarySchema))


class CampaignStatusChangeInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_status_pb2.CampaignStatusChangeInput

    initiator_id = fields.Integer()
    status = PbEnumField(
        enum=CampaignStatusEnum,
        pb_enum=campaign_status_pb2.CampaignStatus.Enum,
        values_map=ENUMS_MAP["campaign_status"],
    )
    note = fields.String(missing="")


class DailyBudgetPredictionInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = DailyBudgetPredictionInput

    campaign_id = fields.Integer()
    start_datetime = PbDateTimeField()
    end_datetime = PbDateTimeField()
    timezone = fields.String(missing="Europe/Moscow")
    budget = PbFixedDecimalDictField(places=4, field="value")


class DailyBudgetPredictionOutputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = DailyBudgetPredictionOutput

    daily_budget = PbFixedDecimalDictField(places=4, field="value")


class UsedSegmentsOutputSchema(StrictSchema):
    usedSegmentIds = fields.List(fields.Integer())


class CampaignForBudgetAnalysisSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignBudgetAnalysisData

    id = fields.Integer()
    budget = PbFixedDecimalDictField(places=4, field="value")
    daily_budget = PbFixedDecimalDictField(places=4, field="value")
    days_left = fields.Integer()

    @post_load
    def postload(self, data):
        return {"campaign_id": data.pop("id"), **data}


class CampaignForBudgetAnalysisListSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignBudgetAnalysisList

    campaigns = fields.Nested(CampaignForBudgetAnalysisSchema, many=True)

    @post_load
    def postload(self, data):
        return data["campaigns"]


class CampaignDataForMonitoringsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignDataForMonitorings

    id = fields.Integer()
    campaign_type = PbEnumField(
        enum=CampaignTypeEnum,
        pb_enum=CampaignType.Enum,
        values_map=ENUMS_MAP["campaign_type"],
    )


class CampaignDataForMonitoringsListSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignDataForMonitoringsList

    campaigns = fields.List(fields.Nested(CampaignDataForMonitoringsSchema))

    @post_load
    def postload(self, data):
        return data["campaigns"]


class CampaignIdListSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_list_pb2.CampaignIdList

    ids = fields.List(fields.Integer())


class CampaignPaidTillChangeInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_pb2.CampaignPaidTillChangeInput

    paid_till = PbDateTimeField(required=False)


class CampaignStatusLogEntrySchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_status_pb2.CampaignStatusLogEntry

    initiator_id = fields.Integer(attribute="author_id")
    status = PbEnumField(
        enum=CampaignStatusEnum,
        pb_enum=campaign_status_pb2.CampaignStatus.Enum,
        values_map=ENUMS_MAP["campaign_status"],
    )
    changed_at = PbDateTimeField(attribute="changed_datetime")
    note = fields.String(attribute="metadata")

    @pre_dump
    def metadata_to_json(self, data):
        data["metadata"] = json.dumps(data["metadata"])
        return data


class CampaignStatusHistorySchema(ProtobufSchema):
    class Meta:
        pb_message_class = campaign_status_pb2.CampaignStatusHistory

    entries = fields.Nested(CampaignStatusLogEntrySchema, many=True)

    @pre_dump
    def to_dict(self, data):
        return {"entries": data}
