import json

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

from maps_adv.billing_proxy.lib.domain import (
    CampaignType,
    CreativeType,
    CurrencyType,
    FixTimeIntervalType,
    OrderSize,
    PlatformType,
    RubricName,
    CpmCoefFieldType,
    TargetingCriterion,
)
from maps_adv.billing_proxy.proto import common_pb2, products_pb2
from maps_adv.common.protomallow import (
    PbDateTimeField,
    PbDecimalField,
    PbEnumField,
    PbFixedDecimalDictField,
    PbFixedDecimalField,
    ProtobufSchema,
    PbTruncatingDecimalField,
)

from .enums_maps import enums_maps


class ProductAdviseInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ProductAdviseInput

    platform = PbEnumField(
        enum=PlatformType,
        pb_enum=common_pb2.PlatformType,
        values_map=enums_maps["platform"],
    )
    campaign_type = PbEnumField(
        enum=CampaignType,
        pb_enum=common_pb2.CampaignType,
        values_map=enums_maps["campaign_type"],
    )
    client_id = fields.Integer()
    contract_id = fields.Integer()
    currency = PbEnumField(
        enum=CurrencyType,
        pb_enum=common_pb2.CurrencyType,
        values_map=enums_maps["currency"],
    )
    service_id = fields.Integer()
    additional_platforms = fields.List(
        PbEnumField(
            enum=PlatformType,
            pb_enum=common_pb2.PlatformType,
            values_map=enums_maps["platform"],
        )
    )

    @validates_schema
    def validate_input(self, data, **kwargs):
        if bool("client_id" in data) != bool("contract_id" in data):
            raise ValidationError(
                "client_id and contract_id must be in(ex)cluded simultaneously"
            )

        if bool("currency" not in data) != bool(
            "client_id" in data and "contract_id" in data
        ):
            raise ValidationError(
                "currency mustn't be provided with client_id or contract_id"
            )


class FixSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.Fix

    time_interval = PbEnumField(
        enum=FixTimeIntervalType,
        pb_enum=products_pb2.Fix.TimeIntervalType,
        values_map=enums_maps["time_interval"],
    )
    OBSOLETE__cost = PbFixedDecimalDictField(places=4, field="value", quantize=True)
    cost = PbTruncatingDecimalField(places=10)

    @pre_dump
    def fill_obsolete_fields(self, data):
        data["OBSOLETE__cost"] = data["cost"]
        return data


class CpmSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.Cpm

    OBSOLETE__base_cpm = PbFixedDecimalDictField(places=4, field="value", quantize=True)
    base_cpm = PbTruncatingDecimalField(places=10)

    @pre_dump
    def fill_obsolete_fields(self, data):
        data["OBSOLETE__base_cpm"] = data["base_cpm"]
        return data


class BillingSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.Billing

    fix = fields.Nested(FixSchema, required=False)
    cpm = fields.Nested(CpmSchema, required=False)


class ProductVersionSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ProductVersion

    version = fields.Integer()
    billing = fields.Nested(BillingSchema)
    active_from = PbDateTimeField()
    active_to = PbDateTimeField(null=True)
    min_budget = PbDecimalField(places=10)
    cpm_filters = fields.List(fields.String())


class ProductInfoSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ProductInfo

    id = fields.Integer()
    oracle_id = fields.Integer()
    service_id = fields.Integer()
    version = fields.Integer()
    title = fields.String()
    act_text = fields.String()
    description = fields.String()
    currency = PbEnumField(
        enum=CurrencyType,
        pb_enum=common_pb2.CurrencyType,
        values_map=enums_maps["currency"],
        by_value=True,
    )
    billing = fields.Nested(BillingSchema)
    vat_value = PbFixedDecimalField(places=4)
    campaign_type = PbEnumField(
        enum=CampaignType,
        pb_enum=common_pb2.CampaignType,
        values_map=enums_maps["campaign_type"],
        by_value=True,
    )
    platform = PbEnumField(
        enum=PlatformType,
        pb_enum=common_pb2.PlatformType,
        values_map=enums_maps["platform"],
        by_value=True,
    )
    platforms = fields.List(
        PbEnumField(
            enum=PlatformType,
            pb_enum=common_pb2.PlatformType,
            values_map=enums_maps["platform"],
        )
    )

    OBSOLETE__min_budget = PbFixedDecimalDictField(
        places=4, field="value", quantize=True
    )
    min_budget = PbDecimalField(places=10)
    comment = fields.String()
    available_for_agencies = fields.Boolean()
    available_for_internal = fields.Boolean()
    active_from = PbDateTimeField()
    active_to = PbDateTimeField(null=True)
    versions = fields.List(fields.Nested(ProductVersionSchema))
    type = fields.String()

    @pre_dump
    def fill_obsolete_fields(self, data):
        data["OBSOLETE__min_budget"] = data["min_budget"]
        data["platform"] = data["platforms"][0]
        return data


class ProductsInfoSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ProductsInfo

    products = fields.List(fields.Nested(ProductInfoSchema))

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


class CpmCalculationInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.CpmCalculationInput

    product_id = fields.Integer()
    rubric = PbEnumField(
        enum=RubricName,
        pb_enum=products_pb2.CpmCalculationInput.AdvRubric,
        values_map=enums_maps["rubric"],
        by_value=True,
        required=False,
    )
    order_size = PbEnumField(
        enum=OrderSize,
        pb_enum=products_pb2.CpmCalculationInput.OrderSize,
        values_map=enums_maps["order_size"],
        by_value=True,
        required=False,
    )
    targeting_query = fields.String(required=False)
    creative_types = fields.List(
        PbEnumField(
            enum=CreativeType,
            pb_enum=products_pb2.CpmCalculationInput.CreativeType,
            values_map=enums_maps["creative_type"],
            by_value=True,
        )
    )
    dt = PbDateTimeField(null=True)
    active_from = PbDateTimeField(required=False)
    active_to = PbDateTimeField(required=False)

    @post_load
    def targeting_query_to_json(self, data):
        if "targeting_query" in data:
            data["targeting_query"] = json.loads(data["targeting_query"])

        return data


class CpmCalculationCoefSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.CpmCalculationCoef

    rate = PbDecimalField(places=10)
    field = PbEnumField(
        enum=CpmCoefFieldType,
        pb_enum=products_pb2.CpmCalculationCoef.FieldType,
        values_map=enums_maps["cpm_coef_field_type"],
        by_value=True,
    )
    default_value = fields.Boolean(required=False)
    targeting_value = PbEnumField(
        enum=TargetingCriterion,
        pb_enum=products_pb2.CpmCalculationCoef.TargetingCriterion,
        values_map=enums_maps["targeting_criterion"],
        by_value=True,
        required=False,
    )
    rubric_value = PbEnumField(
        enum=RubricName,
        pb_enum=products_pb2.CpmCalculationInput.AdvRubric,
        values_map=enums_maps["rubric"],
        by_value=True,
        required=False,
    )
    creative_value = PbEnumField(
        enum=CreativeType,
        pb_enum=products_pb2.CpmCalculationInput.CreativeType,
        values_map=enums_maps["creative_type"],
        by_value=True,
        required=False,
    )
    month_value = fields.Integer(required=False)
    active_from = PbDateTimeField(required=False)
    active_to = PbDateTimeField(required=False)

    @pre_dump
    def to_dict(self, data):
        result = {
            "active_from": data.active_from,
            "active_to": data.active_to,
            "rate": data.rate,
            "field": data.field,
        }

        if data.value is None:
            result["default_value"] = True
        elif data.field == CpmCoefFieldType.CREATIVE:
            result["creative_value"] = data.value
        elif data.field == CpmCoefFieldType.RUBRIC:
            result["rubric_value"] = data.value
        elif data.field == CpmCoefFieldType.TARGETING:
            result["targeting_value"] = data.value
        elif data.field == CpmCoefFieldType.MONTHLY:
            result["month_value"] = data.value

        return result


class CpmCalculationPeriodSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.CpmCalculationPeriod

    active_from = PbDateTimeField()
    active_to = PbDateTimeField()
    final_cpm = PbDecimalField(places=10)


class CpmCalculationResultSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.CpmCalculationResult

    OBSOLETE__cpm = PbFixedDecimalDictField(places=4, field="value", quantize=True)
    cpm = PbDecimalField(places=10)
    base_cpm = PbDecimalField(places=10, required=False)
    coefs = fields.List(fields.Nested(CpmCalculationCoefSchema))
    periods = fields.List(fields.Nested(CpmCalculationPeriodSchema))

    @pre_dump
    def fill_in_obsolete_fields(self, data):
        data["OBSOLETE__cpm"] = data["cpm"]
        return data


class ServiceIdsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ServiceIds

    service_ids = fields.List(fields.Integer(validate=validate.OneOf([37, 110])))


class ClientBindingSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ClientBinding

    client_id = fields.Integer()
    contract_id = fields.Integer(required=False)


class ClientBindingsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ClientBindings

    clients = fields.List(fields.Nested(ClientBindingSchema))

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


class CreateProductInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.CreateProductInput

    oracle_id = fields.Integer()
    title = fields.String()
    act_text = fields.String()
    description = fields.String()
    currency = PbEnumField(
        enum=CurrencyType,
        pb_enum=common_pb2.CurrencyType,
        values_map=enums_maps["currency"],
        by_value=True,
    )
    vat_value = PbFixedDecimalField(places=4)
    campaign_type = PbEnumField(
        enum=CampaignType,
        pb_enum=common_pb2.CampaignType,
        values_map=enums_maps["campaign_type"],
        by_value=True,
    )
    platforms = fields.List(
        PbEnumField(
            enum=PlatformType,
            pb_enum=common_pb2.PlatformType,
            values_map=enums_maps["platform"],
        )
    )
    comment = fields.String()
    active_from = PbDateTimeField()
    active_to = PbDateTimeField(null=True)
    billing = fields.Nested(BillingSchema)
    min_budget = PbDecimalField(places=10)
    cpm_filters = fields.List(fields.String())
    type = fields.String()
    author_id = fields.Integer()


class ProductIdSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.ProductId

    product_id = fields.Integer()


class UpdateProductInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = products_pb2.UpdateProductInput

    title = fields.String()
    act_text = fields.String()
    description = fields.String()
    vat_value = PbFixedDecimalField(places=4)
    comment = fields.String()
    versions = fields.List(fields.Nested(ProductVersionSchema))
    author_id = fields.Integer()


__all__ = [
    "ProductAdviseInputSchema",
    "ProductInfoSchema",
    "ProductsInfoSchema",
    "CpmCalculationInputSchema",
    "CpmCalculationResultSchema",
    "ServiceIdsSchema",
    "ClientBindingSchema",
    "CreateProductInputSchema",
    "ProductIdSchema",
    "UpdateProductInputSchema",
]
