from operator import itemgetter

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

from maps_adv.billing_proxy.lib.domain import CampaignType, CurrencyType, PlatformType
from maps_adv.billing_proxy.proto import (
    common_pb2,
    orders_charge_pb2,
    orders_for_stat_pb2,
    orders_pb2,
)
from maps_adv.common.protomallow import (
    PbDateTimeField,
    PbDecimalField,
    PbEnumField,
    PbFixedDecimalDictField,
    ProtobufSchema,
)

from .enums_maps import enums_maps


class OrderCreationInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.OrderCreationInput

    title = fields.String(validate=validate.Length(max=256))
    client_id = fields.Integer()
    agency_id = fields.Integer()
    product_id = fields.Integer()
    contract_id = fields.Integer(missing=None)
    daily_budget = PbFixedDecimalDictField(places=4, field="value")
    text = fields.String(validate=validate.Length(max=10000), missing="")
    comment = fields.String(validate=validate.Length(max=1024), missing="")
    service_id = fields.Integer(validate=validate.OneOf([37, 110]), missing=110)


class OrderSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.Order

    id = fields.Integer()
    title = fields.String()
    created_at = PbDateTimeField()
    client_id = fields.Integer()
    agency_id = fields.Integer()
    contract_id = fields.Integer()
    product_id = fields.Integer()
    daily_budget = PbFixedDecimalDictField(places=4, field="value")
    text = fields.String()
    comment = fields.String()
    OBSOLETE__limit = PbFixedDecimalDictField(places=4, field="value", quantize=True)
    OBSOLETE__consumed = PbFixedDecimalDictField(places=4, field="value", quantize=True)
    limit = PbDecimalField(places=10)
    consumed = PbDecimalField(places=10)
    external_id = fields.Integer()
    service_id = fields.Integer()
    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"],
        )
    )
    currency = PbEnumField(
        enum=CurrencyType,
        pb_enum=common_pb2.CurrencyType,
        values_map=enums_maps["currency"],
        by_value=True,
    )

    @pre_dump
    def fill_obsolete_fields(self, data):
        data["OBSOLETE__limit"] = data["limit"]
        data["OBSOLETE__consumed"] = data["consumed"]
        data["platform"] = data["platforms"][0]
        return data


class OrdersSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.Orders

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

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


class OrderDepositRequestCreationInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.OrderDepositRequestCreationInput

    OBSOLETE__amount = PbFixedDecimalDictField(places=4, field="value", quantize=True)
    amount = PbDecimalField(places=10)
    region = fields.String(required=False)

    @post_load
    def support_obsolete_fields(self, data):
        if "amount" not in data:
            try:
                data["amount"] = data.pop("OBSOLETE__amount")
            except KeyError:
                raise ValidationError(
                    "At least one of amount and OBSOLETE__amount required"
                )

        return data


class OrderDepositRequestSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.OrderDepositRequest

    request_id = fields.Integer()
    user_url = fields.String()
    admin_url = fields.String()


class OrdersStatInfoInput(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrdersStatInfoInput

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


class OrderStatInfo(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrderStatInfo

    order_id = fields.Integer()
    balance = PbDecimalField(places=10)


class OrdersStatInfo(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrdersStatInfo

    orders_info = fields.Nested(OrderStatInfo, many=True)

    @pre_dump
    def to_flat(self, data):
        orders_info = []

        for id_, data in data.items():
            orders_info.append(data.copy())
            orders_info[-1]["order_id"] = id_

        return {"orders_info": sorted(orders_info, key=itemgetter("order_id"))}


class OrderChargeInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_charge_pb2.OrderChargeInput

    order_id = fields.Integer()
    OBSOLETE__charged_amount = PbFixedDecimalDictField(
        places=4, field="value", required=False
    )
    charged_amount = PbDecimalField(places=10, required=False)

    @post_load
    def support_obsolete_fields(self, data):
        if "charged_amount" not in data:
            try:
                data["charged_amount"] = data.pop("OBSOLETE__charged_amount")
            except KeyError:
                raise ValidationError(
                    "At least one of charged_amount "
                    "and OBSOLETE__charged_amount required"
                )

        return data


class OrdersChargeInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_charge_pb2.OrdersChargeInput

    orders_charge = fields.Nested(OrderChargeInputSchema, many=True)
    bill_for_timestamp = PbDateTimeField()


class OrderChargeOutputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_charge_pb2.OrderChargeOutput

    order_id = fields.Integer()
    success = fields.Boolean()


class OrdersChargeOutputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_charge_pb2.OrdersChargeOutput

    charge_result = fields.Nested(OrderChargeOutputSchema, many=True)
    applied = fields.Boolean()

    @pre_dump
    def charge_result_to_flat(self, data):
        data["charge_result"] = list(
            {"order_id": order_id, "success": result}
            for order_id, result in data["charge_result"].items()
        )
        data["charge_result"].sort(key=itemgetter("order_id"))

        return data


class OrderIdsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.OrderIds

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

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


class OrderUpdateInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.OrderUpdateInput

    title = fields.String()
    text = fields.String()
    comment = fields.String()


class AccountManagerIdSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_pb2.AccountManagerId

    account_manager_id = fields.Integer()


class OrdersDiscountInfoInput(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrdersDiscountInfoInput

    order_ids = fields.List(fields.Integer())
    billed_at = PbDateTimeField()


class OrderDiscountInfo(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrderDiscountInfo

    order_id = fields.Integer()
    discount = PbDecimalField(places=10)


class OrdersDiscountInfo(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrdersDiscountInfo

    discount_info = fields.Nested(OrderDiscountInfo, many=True)

    @pre_dump
    def to_flat(self, data):
        discount_info = []

        for id_, data in data.items():
            discount_info.append(data.copy())
            discount_info[-1]["order_id"] = id_

        return {"discount_info": sorted(discount_info, key=itemgetter("order_id"))}


class DebitInfoSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.DebitInfo

    billed_at = PbDateTimeField()
    amount = PbDecimalField(places=10)


class OrderDebitsInfoSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrderDebitsInfo

    order_id = fields.Integer()
    debits = fields.Nested(DebitInfoSchema, many=True)


class OrdersDebitsInfoSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrdersDebitsInfo

    orders_debits = fields.Nested(OrderDebitsInfoSchema, many=True)


class OrdersDebitsInfoInputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = orders_for_stat_pb2.OrdersDebitsInfoInput

    order_ids = fields.List(fields.Integer())
    billed_after = PbDateTimeField()
