import json

from odoo import _, api, fields, models
from odoo.exceptions import UserError


class ChooseDeliveryFusionPackage(models.TransientModel):
    """One package row in the Add Shipping wizard."""

    _name = 'choose.delivery.fusion.package'
    _description = 'Shipping Package (Wizard)'
    _order = 'sequence, id'

    wizard_id = fields.Many2one(
        'choose.delivery.carrier',
        string='Wizard',
        ondelete='cascade',
    )
    sequence = fields.Integer(default=10)
    package_type_id = fields.Many2one(
        'stock.package.type',
        string='Box Type',
        domain="[('package_carrier_type', '=', 'fusion_canada_post')]",
    )
    package_length = fields.Float(string='Length')
    package_width = fields.Float(string='Width')
    package_height = fields.Float(string='Height')
    weight = fields.Float(string='Weight')

    # Per-package cost for the selected service (updated on service select)
    selected_price = fields.Float(
        string='Cost', digits='Product Price', readonly=True)
    currency_id = fields.Many2one(
        'res.currency',
        related='wizard_id.currency_id',
    )

    @api.onchange('package_type_id')
    def _onchange_package_type_id(self):
        """Pre-fill dimensions from selected box type."""
        if self.package_type_id:
            self.package_length = self.package_type_id.packaging_length
            self.package_width = self.package_type_id.width
            self.package_height = self.package_type_id.height


class ChooseDeliveryFusionRate(models.TransientModel):
    _name = 'choose.delivery.fusion.rate'
    _description = 'Shipping Rate Option'

    wizard_id = fields.Many2one(
        'choose.delivery.carrier',
        string='Wizard',
        ondelete='cascade',
    )
    service_code = fields.Char(string='Service Code')
    service_name = fields.Char(string='Service')
    price = fields.Float(string='Shipping Cost', digits='Product Price')
    expected_delivery = fields.Char(string='Expected Delivery Date')
    is_selected = fields.Boolean(string='Selected', default=False)
    currency_id = fields.Many2one(
        'res.currency',
        related='wizard_id.currency_id',
    )
    # JSON: [{"pkg_id": <int>, "price": <float>}, ...]
    per_package_prices = fields.Text(string='Per-Package Prices')

    def action_select(self):
        """Select this rate and deselect others. Update per-package costs."""
        self.ensure_one()
        # Deselect all, then select this one
        self.wizard_id.fusion_rate_ids.write({'is_selected': False})
        self.is_selected = True

        # Update per-package costs from stored JSON
        if self.per_package_prices:
            try:
                pkg_prices = json.loads(self.per_package_prices)
                for pp in pkg_prices:
                    pkg = self.env['choose.delivery.fusion.package'].browse(
                        pp['pkg_id'])
                    if pkg.exists():
                        pkg.selected_price = pp['price']
            except (json.JSONDecodeError, KeyError):
                pass

        # Apply margin from the carrier
        carrier = self.wizard_id.carrier_id
        price = self.price
        if carrier:
            price = carrier._apply_margins(price, self.wizard_id.order_id)
            # Check free_over
            if carrier.free_over:
                order = self.wizard_id.order_id
                amount = order.currency_id._convert(
                    order.amount_untaxed,
                    order.company_id.currency_id,
                    order.company_id,
                    fields.Date.today(),
                )
                if amount >= carrier.amount:
                    price = 0.0

        self.wizard_id.write({
            'delivery_price': price,
            'display_price': price,
            'fusion_selected_service': self.service_code,
            'fusion_selected_service_name': self.service_name,
            'fusion_selected_expected_delivery': self.expected_delivery,
        })
        # Re-open the wizard to show updated selection
        return {
            'name': _('Add a shipping method'),
            'type': 'ir.actions.act_window',
            'res_model': 'choose.delivery.carrier',
            'res_id': self.wizard_id.id,
            'view_mode': 'form',
            'target': 'new',
        }


class ChooseDeliveryCarrier(models.TransientModel):
    _inherit = 'choose.delivery.carrier'

    fusion_rate_ids = fields.One2many(
        'choose.delivery.fusion.rate',
        'wizard_id',
        string='Available Services',
    )
    fusion_selected_service = fields.Char(
        string='Selected Service Code',
    )
    fusion_selected_service_name = fields.Char(
        string='Selected Service Name',
    )
    fusion_selected_expected_delivery = fields.Char(
        string='Selected Expected Delivery',
    )

    # -- Package list --
    fusion_package_ids = fields.One2many(
        'choose.delivery.fusion.package',
        'wizard_id',
        string='Packages',
    )

    # -- Unit labels --
    fusion_dimension_unit_label = fields.Char(
        string='Dimension Unit',
        compute='_compute_fusion_dimension_unit_label',
    )
    fusion_weight_unit_label = fields.Char(
        string='Weight Unit',
        compute='_compute_fusion_weight_unit_label',
    )

    @api.depends('carrier_id')
    def _compute_fusion_dimension_unit_label(self):
        for rec in self:
            if (rec.carrier_id
                    and rec.carrier_id.delivery_type == 'fusion_canada_post'):
                rec.fusion_dimension_unit_label = (
                    rec.carrier_id.fusion_cp_dimension_unit or 'cm')
            else:
                rec.fusion_dimension_unit_label = ''

    @api.depends('carrier_id')
    def _compute_fusion_weight_unit_label(self):
        for rec in self:
            if (rec.carrier_id
                    and rec.carrier_id.delivery_type == 'fusion_canada_post'):
                uom = (rec.order_id.company_id
                       .weight_unit_of_measurement_id)
                rec.fusion_weight_unit_label = uom.name if uom else 'kg'
            else:
                rec.fusion_weight_unit_label = ''

    @api.onchange('carrier_id')
    def _onchange_carrier_id_fusion_packages(self):
        """When a CP carrier is selected, create one default package."""
        if (self.carrier_id
                and self.carrier_id.delivery_type == 'fusion_canada_post'
                and not self.fusion_package_ids):
            vals = {
                'sequence': 10,
                'weight': self.total_weight or 0.0,
            }
            # Pre-fill from default package type if set on carrier
            if self.carrier_id.product_packaging_id:
                pkg = self.carrier_id.product_packaging_id
                vals['package_type_id'] = pkg.id
                vals['package_length'] = pkg.packaging_length
                vals['package_width'] = pkg.width
                vals['package_height'] = pkg.height
            self.fusion_package_ids = [(5, 0, 0), (0, 0, vals)]

    # -- Rate fetching --

    def update_price(self):
        """Override: for Canada Post, fetch all rates for all packages."""
        if self.carrier_id.delivery_type == 'fusion_canada_post':
            return self._update_fusion_rates()
        return super().update_price()

    def _get_fusion_package_info_for_pkg(self, pkg):
        """Build package_info dict for a single package, converted to cm."""
        carrier = self.carrier_id
        return {
            'length': round(carrier._fusion_cp_convert_dimension_to_cm(
                pkg.package_length), 1),
            'width': round(carrier._fusion_cp_convert_dimension_to_cm(
                pkg.package_width), 1),
            'height': round(carrier._fusion_cp_convert_dimension_to_cm(
                pkg.package_height), 1),
        }

    def _update_fusion_rates(self):
        """Fetch shipping service rates for every package and aggregate."""
        carrier = self.carrier_id
        packages = self.fusion_package_ids

        if not packages:
            raise UserError(_(
                "Please add at least one package with dimensions."))

        from_unit = (self.order_id.company_id
                     .weight_unit_of_measurement_id)

        # -- Validate every package --
        for pkg in packages:
            if not (pkg.package_length and pkg.package_width
                    and pkg.package_height):
                raise UserError(_(
                    "Please enter dimensions (L x W x H) "
                    "for all packages."))
            if not pkg.weight:
                raise UserError(_(
                    "Please enter weight for all packages."))
            package_info = self._get_fusion_package_info_for_pkg(pkg)
            weight_kg = pkg.weight
            if from_unit:
                weight_kg = round(carrier.convert_weight(
                    from_unit, carrier.weight_uom_id, weight_kg), 2)
            carrier._fusion_cp_validate_package(weight_kg, package_info)

        # -- Clear old rates --
        self.fusion_rate_ids.unlink()

        # -- Fetch rates per package --
        # {service_code: {service_name, packages: [{pkg_id, price, exp}]}}
        all_service_rates = {}

        for pkg in packages:
            package_info = self._get_fusion_package_info_for_pkg(pkg)
            carrier_ctx = carrier.with_context(
                order_weight=pkg.weight,      # raw, in company UOM
                cp_package_info=package_info,
            )
            rates = carrier_ctx.fusion_canada_post_rate_shipment_all(
                self.order_id)

            if isinstance(rates, dict) and rates.get('error_message'):
                raise UserError(rates['error_message'])

            if not rates:
                raise UserError(_(
                    "No shipping prices available for this order."))

            for rate in rates:
                code = rate['service_code']
                if code not in all_service_rates:
                    all_service_rates[code] = {
                        'service_name': rate['service_name'],
                        'packages': [],
                    }
                all_service_rates[code]['packages'].append({
                    'pkg_id': pkg.id,
                    'price': rate['price'],
                    'expected_delivery': rate.get(
                        'expected_delivery', ''),
                })

        # -- Keep only services available for ALL packages --
        num_packages = len(packages)
        available = {
            code: data
            for code, data in all_service_rates.items()
            if len(data['packages']) == num_packages
        }

        if not available:
            raise UserError(_(
                "No single shipping service covers all packages. "
                "Try adjusting package dimensions or weight."))

        # -- Find cheapest service --
        cheapest_code = min(
            available.keys(),
            key=lambda c: sum(
                p['price'] for p in available[c]['packages']))

        # -- Create combined rate lines --
        vals_list = []
        for code, data in available.items():
            total_price = sum(p['price'] for p in data['packages'])
            expected_dates = [
                p['expected_delivery'] for p in data['packages']
                if p['expected_delivery']
            ]
            expected = max(expected_dates) if expected_dates else ''
            is_sel = (code == cheapest_code)
            vals_list.append({
                'wizard_id': self.id,
                'service_code': code,
                'service_name': data['service_name'],
                'price': total_price,
                'expected_delivery': expected,
                'is_selected': is_sel,
                'per_package_prices': json.dumps(data['packages']),
            })

        self.env['choose.delivery.fusion.rate'].create(vals_list)

        # -- Auto-select cheapest: update packages and wizard --
        cheapest_data = available[cheapest_code]
        selected_price = sum(
            p['price'] for p in cheapest_data['packages'])

        for pkg_data in cheapest_data['packages']:
            pkg_rec = packages.filtered(
                lambda p, pid=pkg_data['pkg_id']: p.id == pid)
            if pkg_rec:
                pkg_rec.selected_price = pkg_data['price']

        # Apply carrier margins
        price = selected_price
        if self.carrier_id:
            price = self.carrier_id._apply_margins(
                price, self.order_id)
            if self.carrier_id.free_over:
                amount = self.order_id.currency_id._convert(
                    self.order_id.amount_untaxed,
                    self.order_id.company_id.currency_id,
                    self.order_id.company_id,
                    fields.Date.today(),
                )
                if amount >= self.carrier_id.amount:
                    price = 0.0

        expected_dates = [
            p['expected_delivery'] for p in cheapest_data['packages']
            if p['expected_delivery']
        ]

        self.write({
            'delivery_price': price,
            'display_price': price,
            'fusion_selected_service': cheapest_code,
            'fusion_selected_service_name': (
                cheapest_data['service_name']),
            'fusion_selected_expected_delivery': (
                max(expected_dates) if expected_dates else ''),
        })

        return {
            'name': _('Add a shipping method'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'choose.delivery.carrier',
            'res_id': self.id,
            'target': 'new',
        }

    # -- Confirm --

    def button_confirm(self):
        """Override: store per-package info on the sale order and enhance
        the delivery line description."""
        if self.carrier_id.delivery_type == 'fusion_canada_post':
            order = self.order_id

            # Clear previous package records
            order.fusion_package_ids.unlink()

            selected_code = self.fusion_selected_service or ''
            selected_name = self.fusion_selected_service_name or ''
            selected_delivery = (
                self.fusion_selected_expected_delivery or '')

            pkg_vals = []
            for idx, pkg in enumerate(
                    self.fusion_package_ids.sorted('sequence')):
                pkg_vals.append((0, 0, {
                    'sequence': (idx + 1) * 10,
                    'package_type_id': (
                        pkg.package_type_id.id
                        if pkg.package_type_id else False),
                    'package_length': pkg.package_length,
                    'package_width': pkg.package_width,
                    'package_height': pkg.package_height,
                    'weight': pkg.weight,
                    'service_code': selected_code,
                    'service_name': selected_name,
                    'price': pkg.selected_price,
                    'expected_delivery': selected_delivery,
                }))

            write_vals = {
                'fusion_package_ids': pkg_vals,
                'fusion_cp_service_code': selected_code,
            }

            # Backward compat: first-package dims in legacy fields
            first_pkg = self.fusion_package_ids.sorted('sequence')[:1]
            if first_pkg:
                write_vals['fusion_cp_package_length'] = (
                    first_pkg.package_length)
                write_vals['fusion_cp_package_width'] = (
                    first_pkg.package_width)
                write_vals['fusion_cp_package_height'] = (
                    first_pkg.package_height)

            order.write(write_vals)

        res = super().button_confirm()

        # Enhance delivery line description with service details
        if (self.carrier_id.delivery_type == 'fusion_canada_post'
                and self.fusion_selected_service_name):
            delivery_line = self.order_id.order_line.filtered(
                'is_delivery')
            if delivery_line:
                line = delivery_line[-1]
                parts = [line.name]
                parts.append(
                    "Service: %s"
                    % self.fusion_selected_service_name)
                num_pkgs = len(self.fusion_package_ids)
                if num_pkgs > 1:
                    parts.append("Packages: %d" % num_pkgs)
                if self.fusion_selected_expected_delivery:
                    parts.append(
                        "Expected Delivery: %s"
                        % self.fusion_selected_expected_delivery)
                line.name = '\n'.join(parts)
        return res
