import base64
import logging
from datetime import datetime as dt_mod
from lxml import etree
from requests import request
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from odoo.addons.fusion_shipping.api.canada_post.response import Response

_logger = logging.getLogger(__name__)


class FusionShipment(models.Model):
    _name = 'fusion.shipment'
    _description = 'Fusion Shipment'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'shipment_date desc, id desc'
    _rec_name = 'name'

    name = fields.Char(
        string='Reference',
        required=True,
        readonly=True,
        default=lambda self: _('New'),
        copy=False,
    )
    carrier_type = fields.Selection(
        [
            ('canada_post', 'Canada Post'),
            ('ups', 'UPS'),
            ('ups_rest', 'UPS (REST)'),
            ('fedex', 'FedEx'),
            ('fedex_rest', 'FedEx (REST)'),
            ('dhl', 'DHL Express'),
            ('dhl_rest', 'DHL Express (REST)'),
        ],
        string='Carrier Type',
        readonly=True,
        tracking=True,
        help='The shipping carrier used for this shipment.',
    )
    tracking_number = fields.Char(
        string='Tracking Number',
        index=True,
        readonly=True,
        copy=False,
        tracking=True,
    )
    shipment_id = fields.Char(
        string='CP Shipment ID',
        readonly=True,
        copy=False,
        help='Canada Post internal shipment identifier',
    )
    sale_order_id = fields.Many2one(
        'sale.order',
        string='Sale Order',
        index=True,
        ondelete='set null',
        tracking=True,
    )
    picking_id = fields.Many2one(
        'stock.picking',
        string='Transfer',
        index=True,
        ondelete='set null',
        tracking=True,
    )
    carrier_id = fields.Many2one(
        'delivery.carrier',
        string='Carrier',
        ondelete='set null',
    )
    shipment_date = fields.Datetime(
        string='Shipment Date',
        default=fields.Datetime.now,
        tracking=True,
    )
    status = fields.Selection(
        [
            ('draft', 'Draft'),
            ('confirmed', 'Confirmed'),
            ('shipped', 'Shipped'),
            ('delivered', 'Delivered'),
            ('returned', 'Returned'),
            ('cancelled', 'Cancelled'),
        ],
        string='Status',
        default='confirmed',
        tracking=True,
    )
    # Label attachments (Many2one for storage)
    label_attachment_id = fields.Many2one(
        'ir.attachment',
        string='Printable Label (4x6)',
        ondelete='set null',
        help='The thermal printer label (4x6 format)',
    )
    full_label_attachment_id = fields.Many2one(
        'ir.attachment',
        string='Full Label (8.5x11)',
        ondelete='set null',
        help='Full page label with instructions and receipt',
    )
    receipt_attachment_id = fields.Many2one(
        'ir.attachment',
        string='Receipt',
        ondelete='set null',
    )
    commercial_invoice_attachment_id = fields.Many2one(
        'ir.attachment',
        string='Commercial Invoice',
        ondelete='set null',
    )
    # Return label
    return_tracking_number = fields.Char(
        string='Return Tracking Number',
        readonly=True,
        copy=False,
    )
    return_label_attachment_id = fields.Many2one(
        'ir.attachment',
        string='Return Label',
        ondelete='set null',
    )
    # Shipment details
    shipping_cost = fields.Monetary(
        string='Shipping Cost',
        currency_field='currency_id',
        readonly=True,
    )
    service_type = fields.Char(
        string='Service Type',
        readonly=True,
    )
    weight = fields.Float(
        string='Weight',
        digits='Stock Weight',
        readonly=True,
    )
    package_name = fields.Char(
        string='Package',
        readonly=True,
        help='Name of the package this shipment covers',
    )
    # Address fields (computed)
    sender_name = fields.Char(
        string='Sender',
        compute='_compute_sender_fields',
        store=True,
    )
    sender_address = fields.Char(
        string='Sender Address',
        compute='_compute_sender_fields',
        store=True,
    )
    recipient_name = fields.Char(
        string='Recipient',
        compute='_compute_recipient_fields',
        store=True,
    )
    recipient_address = fields.Char(
        string='Recipient Address',
        compute='_compute_recipient_fields',
        store=True,
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env.company,
    )
    currency_id = fields.Many2one(
        'res.currency',
        related='company_id.currency_id',
        store=True,
    )
    # Tracking history
    tracking_event_ids = fields.One2many(
        'fusion.tracking.event',
        'shipment_id',
        string='Tracking Events',
    )
    tracking_event_count = fields.Integer(
        string='Tracking Events',
        compute='_compute_tracking_event_count',
    )
    last_tracking_update = fields.Datetime(
        string='Last Tracking Update',
        readonly=True,
    )
    delivery_date = fields.Datetime(
        string='Delivery Date',
        readonly=True,
    )

    def _compute_tracking_event_count(self):
        for rec in self:
            rec.tracking_event_count = len(rec.tracking_event_ids)

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get('name', _('New')) == _('New'):
                vals['name'] = self.env['ir.sequence'].next_by_code(
                    'fusion.shipment'
                ) or _('New')
        return super().create(vals_list)

    @api.depends('picking_id', 'picking_id.picking_type_id.warehouse_id.partner_id')
    def _compute_sender_fields(self):
        for rec in self:
            partner = (
                rec.picking_id.picking_type_id.warehouse_id.partner_id
                if rec.picking_id
                else False
            )
            if partner:
                rec.sender_name = partner.name or ''
                parts = filter(None, [
                    partner.street,
                    partner.city,
                    partner.state_id.name if partner.state_id else '',
                    partner.zip,
                ])
                rec.sender_address = ', '.join(parts)
            else:
                rec.sender_name = ''
                rec.sender_address = ''

    @api.depends('picking_id', 'picking_id.partner_id')
    def _compute_recipient_fields(self):
        for rec in self:
            partner = rec.picking_id.partner_id if rec.picking_id else False
            if partner:
                rec.recipient_name = partner.name or ''
                parts = filter(None, [
                    partner.street,
                    partner.city,
                    partner.state_id.name if partner.state_id else '',
                    partner.zip,
                ])
                rec.recipient_address = ', '.join(parts)
            else:
                rec.recipient_name = ''
                rec.recipient_address = ''

    def action_open_sale_order(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Sale Order',
            'res_model': 'sale.order',
            'res_id': self.sale_order_id.id,
            'view_mode': 'form',
            'target': 'current',
        }

    def action_open_picking(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Transfer',
            'res_model': 'stock.picking',
            'res_id': self.picking_id.id,
            'view_mode': 'form',
            'target': 'current',
        }

    def _action_open_attachment(self, attachment):
        """Open an attachment PDF in the browser viewer (new tab)."""
        self.ensure_one()
        if not attachment:
            return False
        return {
            'type': 'ir.actions.act_url',
            'url': '/web/content/%s?download=false' % attachment.id,
            'target': 'new',
        }

    def action_view_label(self):
        return self._action_open_attachment(self.label_attachment_id)

    def action_view_full_label(self):
        return self._action_open_attachment(self.full_label_attachment_id)

    def action_view_receipt(self):
        return self._action_open_attachment(self.receipt_attachment_id)

    def action_view_commercial_invoice(self):
        return self._action_open_attachment(self.commercial_invoice_attachment_id)

    # ── Tracking ──────────────────────────────────────────────

    def action_refresh_tracking(self):
        """Fetch latest tracking events from Canada Post VIS API."""
        self.ensure_one()
        if not self.tracking_number:
            raise ValidationError(
                _("No tracking number available for this shipment."))
        carrier = self.carrier_id
        if not carrier:
            raise ValidationError(
                _("No carrier linked to this shipment."))

        # VIS tracking uses /vis/ path, not /rs/
        if carrier.prod_environment:
            base = "https://soa-gw.canadapost.ca"
        else:
            base = "https://ct.soa-gw.canadapost.ca"

        url = "%s/vis/track/pin/%s/detail" % (base, self.tracking_number)
        headers = {
            'Accept': 'application/vnd.cpc.track-v2+xml',
            'Accept-language': 'en-CA',
        }

        try:
            resp = request(
                method='GET', url=url, headers=headers,
                auth=(carrier.username, carrier.password))
            _logger.info("Tracking API %s → %s", url, resp.status_code)

            if resp.status_code == 404:
                # "No Pin History" — normal for new/sandbox shipments
                self.last_tracking_update = fields.Datetime.now()
                self.message_post(
                    body=_("No tracking history available yet for %s.") %
                    self.tracking_number)
                return

            if resp.status_code != 200:
                raise ValidationError(
                    _("Canada Post tracking error: %s %s\n%s") % (
                        resp.status_code, resp.reason,
                        resp.text[:500] if resp.text else ''))
            api_resp = Response(resp)
            result = api_resp.dict()
        except ValidationError:
            raise
        except Exception as e:
            raise ValidationError(
                _("Failed to fetch tracking: %s") % str(e))

        self._process_tracking_events(result)

    def _process_tracking_events(self, result):
        """Parse CP tracking detail response and store events."""
        detail = result.get('tracking-detail', result)
        sig_events = detail.get('significant-events', {})
        occurrences = sig_events.get('occurrence', [])

        # Single event returns dict, multiple returns list
        if isinstance(occurrences, dict):
            occurrences = [occurrences]

        # Replace existing events
        self.tracking_event_ids.unlink()

        vals_list = []
        for occ in occurrences:
            event_date_str = occ.get('event-date', '')
            event_time_str = occ.get('event-time', '')

            # Build combined datetime for sorting
            event_datetime = False
            if event_date_str:
                try:
                    if event_time_str:
                        event_datetime = dt_mod.strptime(
                            '%s %s' % (event_date_str, event_time_str),
                            '%Y-%m-%d %H:%M:%S')
                    else:
                        event_datetime = dt_mod.strptime(
                            event_date_str, '%Y-%m-%d')
                except (ValueError, TypeError):
                    pass

            vals_list.append({
                'shipment_id': self.id,
                'event_date': event_date_str or False,
                'event_time': event_time_str or '',
                'event_datetime': event_datetime,
                'event_description': occ.get('event-description', ''),
                'event_type': occ.get('event-type', ''),
                'event_site': occ.get('event-site', ''),
                'event_province': occ.get('event-province', ''),
                'signatory_name': occ.get('signatory-name', ''),
            })

        if vals_list:
            self.env['fusion.tracking.event'].create(vals_list)

        self.last_tracking_update = fields.Datetime.now()
        self._update_status_from_tracking(detail)
        self.message_post(
            body=_("Tracking refreshed: %d events loaded.") % len(vals_list))

    def _update_status_from_tracking(self, detail):
        """Auto-update shipment status based on tracking data."""
        if self.status == 'cancelled':
            return

        delivered_date = detail.get('actual-delivery-date', '')
        if delivered_date:
            if self._has_return_events():
                self.status = 'returned'
            else:
                self.status = 'delivered'
            try:
                self.delivery_date = dt_mod.strptime(
                    delivered_date, '%Y-%m-%d')
            except (ValueError, TypeError):
                self.delivery_date = fields.Datetime.now()
        elif self._has_return_events():
            self.status = 'returned'
        elif self.status == 'confirmed' and self.tracking_event_ids:
            self.status = 'shipped'

    def _has_return_events(self):
        """Check if any tracking events indicate a return/RTS."""
        RETURN_TYPES = {'RTS', 'RETURN', 'RTS_LABEL_PROC'}
        for event in self.tracking_event_ids:
            if event.event_type in RETURN_TYPES:
                return True
            desc = (event.event_description or '').lower()
            if 'return to sender' in desc or 'item returned' in desc:
                return True
        return False

    # ── Void & Reissue ─────────────────────────────────────

    def action_void_shipment(self):
        """Void this shipment via Canada Post API (DELETE endpoint)."""
        self.ensure_one()
        if self.status == 'cancelled':
            raise ValidationError(
                _("This shipment is already cancelled."))
        if not self.shipment_id:
            raise ValidationError(
                _("No CP shipment ID — cannot void."))

        carrier = self.carrier_id
        if not carrier:
            raise ValidationError(
                _("No carrier linked to this shipment."))

        if carrier.prod_environment:
            base = "https://soa-gw.canadapost.ca"
        else:
            base = "https://ct.soa-gw.canadapost.ca"
        customer = carrier.customer_number

        if carrier.fusion_cp_type == 'commercial':
            url = "%s/rs/%s/%s/shipment/%s" % (
                base, customer, customer, self.shipment_id)
            accept_header = 'application/vnd.cpc.shipment-v8+xml'
        else:
            url = "%s/rs/%s/ncshipment/%s" % (
                base, customer, self.shipment_id)
            accept_header = 'application/vnd.cpc.ncshipment-v4+xml'

        try:
            resp = request(
                method='DELETE', url=url,
                headers={
                    'Accept': accept_header,
                    'Accept-language': 'en-CA',
                },
                auth=(carrier.username, carrier.password))
            _logger.info(
                "Void Shipment %s → %s", url, resp.status_code)

            if resp.status_code == 204:
                self.status = 'cancelled'
                self.message_post(
                    body=_("Shipment voided successfully via "
                           "Canada Post API."))
            else:
                # Parse CP error XML for a clean message
                error_msg = self._parse_cp_error_response(resp)
        except ValidationError:
            raise
        except Exception as e:
            raise ValidationError(
                _("Failed to void shipment: %s") % str(e))

    def _parse_cp_error_response(self, resp):
        """Parse a Canada Post error XML response and raise a clean
        ValidationError with the human-readable description.

        If the response cannot be parsed, falls back to the raw text.
        """
        description = ''
        try:
            root = etree.fromstring(resp.content)
            # Strip namespace for easy tag matching
            ns = {'cp': root.nsmap.get(None, '')}
            if ns['cp']:
                msgs = root.findall('.//cp:message', ns)
            else:
                msgs = root.findall('.//message')
            parts = []
            for msg in msgs:
                desc_el = msg.find(
                    'cp:description', ns) if ns['cp'] else msg.find(
                    'description')
                if desc_el is not None and desc_el.text:
                    parts.append(desc_el.text.strip())
            if parts:
                description = '\n'.join(parts)
        except Exception:
            description = ''

        if not description:
            description = (resp.text[:500]
                           if resp.text else 'Unknown error')

        raise ValidationError(
            _("Canada Post: %s") % description)

    def action_reissue_shipment(self):
        """Void current shipment and create a new one."""
        self.ensure_one()
        if self.status != 'cancelled':
            self.action_void_shipment()
        if not self.picking_id:
            raise ValidationError(
                _("No transfer linked — cannot reissue."))
        picking = self.picking_id
        carrier = picking.carrier_id or self.carrier_id
        if not carrier:
            raise ValidationError(
                _("No carrier found for reissue."))
        result = carrier.send_shipping(picking)
        if result:
            picking.carrier_tracking_ref = result[0].get(
                'tracking_number', '')
        self.message_post(
            body=_("Shipment reissued. See new shipment record."))

    def action_track_on_carrier(self):
        """Open the carrier's tracking website in a new tab."""
        self.ensure_one()
        if not self.tracking_number:
            raise ValidationError(
                _("No tracking number available for this shipment."))
        # Default tracking URLs per carrier type
        DEFAULT_TRACKING = {
            'canada_post': 'https://www.canadapost.ca/trackweb/en#/resultList?searchFor=',
            'ups': 'https://www.ups.com/track?tracknum=',
            'ups_rest': 'https://www.ups.com/track?tracknum=',
            'fedex': 'https://www.fedex.com/wtrk/track/?trknbr=',
            'fedex_rest': 'https://www.fedex.com/wtrk/track/?trknbr=',
            'dhl': 'https://www.dhl.com/en/express/tracking.html?AWB=',
            'dhl_rest': 'https://www.dhl.com/en/express/tracking.html?AWB=',
        }
        base_link = ''
        if self.carrier_id and self.carrier_id.tracking_link:
            base_link = self.carrier_id.tracking_link
        elif self.carrier_type:
            base_link = DEFAULT_TRACKING.get(self.carrier_type, '')
        if not base_link:
            base_link = 'https://www.google.com/search?q='
        return {
            'type': 'ir.actions.act_url',
            'url': '%s%s' % (base_link, self.tracking_number),
            'target': 'new',
        }

    # ── Cron ─────────────────────────────────────────────────

    @api.model
    def _cron_refresh_tracking(self):
        """Auto-refresh tracking for all active (non-terminal) shipments."""
        shipments = self.search([
            ('status', 'in', ('confirmed', 'shipped')),
            ('tracking_number', '!=', False),
        ])
        _logger.info(
            "Cron: refreshing tracking for %d shipments", len(shipments))
        for shipment in shipments:
            try:
                shipment.action_refresh_tracking()
                self.env.cr.commit()
            except Exception as e:
                self.env.cr.rollback()
                _logger.warning(
                    "Cron: tracking refresh failed for %s: %s",
                    shipment.name, str(e))

    # ── Return Labels ────────────────────────────────────────

    def action_view_return_label(self):
        return self._action_open_attachment(self.return_label_attachment_id)

    def action_create_return_label(self):
        """Create a prepaid return label via Canada Post Authorized
        Returns API. Bill-on-scan: charged only when the customer
        uses the label at a post office.
        """
        self.ensure_one()
        if self.return_tracking_number:
            raise ValidationError(
                _("A return label has already been created "
                  "for this shipment."))
        if not self.carrier_id:
            raise ValidationError(
                _("No carrier linked to this shipment."))
        if not self.picking_id:
            raise ValidationError(
                _("No transfer linked to this shipment."))

        carrier = self.carrier_id
        customer = carrier.customer_number

        warehouse_partner = (
            self.picking_id.picking_type_id.warehouse_id.partner_id)
        if not warehouse_partner:
            raise ValidationError(
                _("No warehouse address found. Please configure "
                  "a contact on the warehouse."))

        if carrier.prod_environment:
            base = "https://soa-gw.canadapost.ca"
        else:
            base = "https://ct.soa-gw.canadapost.ca"

        url = "%s/rs/%s/%s/authorizedreturn" % (base, customer, customer)

        xml = self._build_return_label_xml(carrier, warehouse_partner)
        headers = {
            'Content-Type': 'application/vnd.cpc.authreturn-v2+xml',
            'Accept': 'application/vnd.cpc.authreturn-v2+xml',
            'Accept-language': 'en-CA',
        }

        try:
            resp = request(
                method='POST', url=url, data=xml,
                headers=headers,
                auth=(carrier.username, carrier.password))
            _logger.info(
                "Return Label API %s → %s", url, resp.status_code)

            if resp.status_code not in (200, 201):
                self._parse_cp_error_response(resp)

            self._process_return_label_response(resp, carrier)

        except ValidationError:
            raise
        except Exception as e:
            raise ValidationError(
                _("Failed to create return label: %s") % str(e))

    def _build_return_label_xml(self, carrier, return_to_partner):
        """Build Authorized Return XML.

        return_to_partner = warehouse address (where the return goes).
        """
        ns = "http://www.canadapost.ca/ws/authreturn-v2"
        root = etree.Element("authorized-return", xmlns=ns)

        etree.SubElement(root, "service-code").text = "DOM.EP"

        # Receiver — warehouse where the item is returned to
        receiver = etree.SubElement(root, "receiver")
        rec_name = etree.SubElement(receiver, "name")
        rec_name.text = (return_to_partner.name or '')[:44]
        rec_company = etree.SubElement(receiver, "company")
        rec_company.text = (
            return_to_partner.commercial_company_name
            or return_to_partner.name or '')[:44]
        rec_addr = etree.SubElement(receiver, "domestic-address")
        etree.SubElement(rec_addr, "address-line-1").text = (
            return_to_partner.street or '')[:44]
        if return_to_partner.street2:
            etree.SubElement(rec_addr, "address-line-2").text = (
                return_to_partner.street2)[:44]
        etree.SubElement(rec_addr, "city").text = (
            return_to_partner.city or '')[:40]
        etree.SubElement(rec_addr, "province").text = (
            return_to_partner.state_id.code or '')
        etree.SubElement(rec_addr, "postal-code").text = (
            (return_to_partner.zip or '').replace(' ', ''))

        # Parcel characteristics
        parcel = etree.SubElement(root, "parcel-characteristics")
        etree.SubElement(parcel, "weight").text = str(
            max(self.weight or 0.5, 0.1))

        # Print preferences
        prefs = etree.SubElement(root, "print-preferences")
        etree.SubElement(prefs, "output-format").text = (
            carrier.fusion_cp_output_format or '8.5x11')

        return etree.tostring(
            root, xml_declaration=True, encoding='UTF-8')

    def _process_return_label_response(self, resp, carrier):
        """Parse return label response, download label PDF,
        store on shipment.
        """
        api_resp = Response(resp)
        result = api_resp.dict()

        auth_return = result.get('authorized-return', result)
        tracking_pin = auth_return.get('tracking-pin', '')
        self.return_tracking_number = tracking_pin

        # Get label artifact URL from links
        links = auth_return.get('links', {})
        link_list = links.get('link', [])
        if isinstance(link_list, dict):
            link_list = [link_list]

        label_url = ''
        for link in link_list:
            if link.get('@rel') == 'labelDetails':
                label_url = link.get('@href', '')
                break

        if label_url:
            label_resp = request(
                method='GET', url=label_url,
                headers={
                    'Accept': 'application/pdf',
                    'Accept-language': 'en-CA',
                },
                auth=(carrier.username, carrier.password))
            if label_resp.status_code == 200:
                attachment = self.env['ir.attachment'].create({
                    'name': 'Return-Label-%s.pdf' % tracking_pin,
                    'type': 'binary',
                    'datas': base64.b64encode(
                        label_resp.content).decode(),
                    'res_model': self._name,
                    'res_id': self.id,
                    'mimetype': 'application/pdf',
                })
                self.return_label_attachment_id = attachment

        self.message_post(
            body=_("Return label created. Tracking: %s") % tracking_pin,
            attachment_ids=(
                self.return_label_attachment_id.ids
                if self.return_label_attachment_id else []))
