# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)

import base64
import json
import logging
import xml.etree.ElementTree as ET
from collections import OrderedDict
from datetime import datetime

from odoo import api, models

_logger = logging.getLogger(__name__)


class FusionXmlParser(models.AbstractModel):
    """Utility to parse ADP application XML files and create/update
    client profiles and application data records.
    
    Captures ALL ~300 XML fields for round-trip export fidelity.
    """
    _name = 'fusion.xml.parser'
    _description = 'ADP XML Parser'

    # ------------------------------------------------------------------
    # PUBLIC API
    # ------------------------------------------------------------------
    @api.model
    def parse_from_binary(self, binary_data, sale_order=None):
        """Parse from binary field (base64 encoded).
        Returns tuple (profile, application_data) or (False, False).
        """
        if not binary_data:
            return False, False
        try:
            xml_content = base64.b64decode(binary_data).decode('utf-8')
        except Exception as e:
            _logger.warning('Failed to decode XML binary: %s', e)
            return False, False
        return self.parse_and_create(xml_content, sale_order)

    @api.model
    def parse_and_create(self, xml_content, sale_order=None):
        """Parse raw XML string, create/update profile and application data.
        Returns tuple (profile, application_data) or (False, False).
        """
        try:
            root = ET.fromstring(xml_content)
        except ET.ParseError as e:
            _logger.warning('Failed to parse ADP XML: %s', e)
            return False, False

        form = root.find('Form')
        if form is None:
            form = root

        # Step 1: Build complete JSON dict (every field, dot-notation keys)
        json_dict = self._xml_to_json(form)

        # Step 2: Extract individual model fields from JSON
        model_vals = self._json_to_model_vals(json_dict)
        model_vals['raw_xml'] = xml_content
        model_vals['xml_data_json'] = json.dumps(json_dict, ensure_ascii=False)

        # Step 3: Create/update profile
        profile = self._find_or_create_profile(model_vals, sale_order)

        # Step 3b: Auto-link authorizer on sale order by ADP number
        if sale_order:
            self._link_authorizer_by_adp_number(model_vals, sale_order)

        # Step 4: Create application data record
        model_vals['profile_id'] = profile.id
        model_vals['sale_order_id'] = sale_order.id if sale_order else False
        app_data = self.env['fusion.adp.application.data'].create(model_vals)

        return profile, app_data

    @api.model
    def reparse_existing(self, app_data_record):
        """Re-parse an existing application data record from its raw_xml.
        Updates all fields in place without creating a new record.
        """
        if not app_data_record.raw_xml:
            return False
        try:
            root = ET.fromstring(app_data_record.raw_xml)
        except ET.ParseError as e:
            _logger.warning('Failed to re-parse XML: %s', e)
            return False

        form = root.find('Form')
        if form is None:
            form = root

        json_dict = self._xml_to_json(form)
        model_vals = self._json_to_model_vals(json_dict)
        model_vals['xml_data_json'] = json.dumps(json_dict, ensure_ascii=False)

        # Remove fields that shouldn't be overwritten
        model_vals.pop('raw_xml', None)
        model_vals.pop('profile_id', None)
        model_vals.pop('sale_order_id', None)

        app_data_record.write(model_vals)

        # Also update the linked profile
        if app_data_record.profile_id:
            profile_vals = {}
            if model_vals.get('medical_condition'):
                profile_vals['medical_condition'] = model_vals['medical_condition']
            if model_vals.get('mobility_status'):
                profile_vals['mobility_status'] = model_vals['mobility_status']
            if model_vals.get('applicant_first_name'):
                profile_vals['first_name'] = model_vals['applicant_first_name']
            if model_vals.get('applicant_last_name'):
                profile_vals['last_name'] = model_vals['applicant_last_name']
            assessment = model_vals.get('assessment_date')
            if assessment:
                profile_vals['last_assessment_date'] = assessment
            if profile_vals:
                app_data_record.profile_id.write(profile_vals)

        return True

    # ------------------------------------------------------------------
    # STEP 1: XML -> FLAT JSON DICT (every field preserved)
    # ------------------------------------------------------------------
    def _xml_to_json(self, form):
        """Convert the entire Form element to a flat JSON dict with dot-notation keys."""
        d = OrderedDict()
        d['deviceCategory'] = self._t(form, 'deviceCategory')
        d['VersionNumber'] = self._t(form, 'VersionNumber')

        # Section 1
        s1 = form.find('section1')
        if s1 is not None:
            for tag in ['applicantLastname', 'applicantFirstname', 'applicantMiddleinitial',
                        'healthNo', 'versionNo', 'DateOfBirth', 'nameLTCH',
                        'unitNo', 'streetNo', 'streetName', 'rrRoute',
                        'city', 'province', 'postalCode',
                        'homePhone', 'busPhone', 'phoneExtension']:
                d[f'section1.{tag}'] = self._t(s1, tag)
            cob = s1.find('confirmationOfBenefit')
            if cob is not None:
                for tag in ['q1Yn', 'q1Ifyes', 'q2Yn', 'q3Yn']:
                    d[f'section1.confirmationOfBenefit.{tag}'] = self._t(cob, tag)

        # Section 2
        s2 = form.find('section2')
        if s2 is not None:
            de = s2.find('devicesandEligibility')
            if de is not None:
                for tag in ['condition', 'status', 'none', 'forearm', 'wheeled', 'manual',
                            'power', 'addOn', 'scooter', 'seating', 'tiltSystem', 'reclineSystem',
                            'legRests', 'frame', 'stroller', 'deviceForearm', 'deviceWheeled',
                            'deviceManual', 'deviceAmbulation', 'deviceDependent', 'deviceDynamic',
                            'manualDyanmic', 'manualWheelchair', 'powerBase', 'powerScooter',
                            'ambulation', 'positioning', 'highTech', 'standingFrame',
                            'adpFunded', 'nonADPFunded']:
                    d[f'section2.devicesandEligibility.{tag}'] = self._t(de, tag)

            # Section 2a
            s2a = s2.find('section2a')
            if s2a is not None:
                for tag in ['walker', 'paediatricFrame', 'forearmCrutches', 'none',
                            'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
                            'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5', 'confirmation6',
                            'seatHeight', 'seatHeightmeasurement', 'handleHeight', 'handleHeightmeasurement',
                            'handGrips', 'forearm', 'widthHandles', 'widthHandlesmeasurement',
                            'clientWeight', 'clientWeightmeasurement',
                            'brakes', 'brakeType', 'noWheels', 'wheelSize', 'backSupport',
                            'adpWalker', 'adpFrame', 'adpStanding',
                            'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
                            'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
                            'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
                            'custom', 'costLabour']:
                    d[f'section2.section2a.{tag}'] = self._t(s2a, tag)

            # Section 2b
            s2b = s2.find('section2b')
            if s2b is not None:
                for tag in ['baseDevice', 'powerAddOndevice',
                            'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
                            'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5',
                            'confirmation6', 'confirmation7', 'confirmation8', 'confirmation9', 'confirmation10',
                            'confirmation11', 'confirmation12', 'confirmation13',
                            'seatWidth', 'seatWidthmeasurement', 'seatDepth', 'seatDepthmeasurement',
                            'floorHeight', 'floorHeightmeasurement', 'caneHeight', 'caneHeightmeasurement',
                            'backHeight', 'backHeightmeasurement', 'restLength', 'restLengthmeasurement',
                            'clientWeight', 'clientWeightmeasurement',
                            'adjustableTension', 'heavyDuty', 'recliner', 'footplates', 'legrests',
                            'spoke', 'projected', 'standardManual', 'gradeAids', 'casterPin',
                            'amputeeAxle', 'quickRelease', 'stroller', 'oxygen', 'ventilator',
                            'titanium', 'clothingGuards', 'oneArm', 'uniLateral', 'plastic',
                            'rationale',
                            'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
                            'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
                            'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
                            'custom', 'costLabour']:
                    d[f'section2.section2b.{tag}'] = self._t(s2b, tag)

            # Section 2c
            s2c = s2.find('section2c')
            if s2c is not None:
                for tag in ['baseDevice',
                            'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
                            'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5',
                            'seatWidth', 'seatWidthmeasurement', 'backHeight', 'backHeightmeasurement',
                            'floorHeight', 'floorHeightmeasurement', 'restLength', 'restLengthmeasurement',
                            'seatDepth', 'seatDepthmeasurement', 'clientWeight', 'clientWeightmeasurement',
                            'adjustableTension', 'midline', 'manualRecline', 'footplates', 'legrests',
                            'swingaway', 'onePiece', 'seatPackage1', 'seatPackage2', 'oxygen', 'ventilator',
                            'spControls1', 'spControls2', 'spControls3', 'spControls4', 'spControls5', 'spControls6',
                            'autoCorrection', 'rationale',
                            'powerTilt', 'powerRecline', 'tiltAndRecline', 'powerElevating', 'ControlBox',
                            'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
                            'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
                            'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
                            'custom', 'costLabour']:
                    d[f'section2.section2c.{tag}'] = self._t(s2c, tag)

            # Section 2d
            s2d = s2.find('section2d')
            if s2d is not None:
                for tag in ['seatM', 'seatCF', 'coverM', 'coverCF', 'optionM', 'optionCF', 'hardwareM', 'hardwareCF',
                            'adductorM', 'adductorCF', 'pommelCF',
                            'backM', 'backCF', 'supportoptionM', 'supportoptionCF', 'backcoverCF', 'backHardwareM', 'backHardwareCF',
                            'completeM', 'completeCF',
                            'headrestM', 'headrestCF', 'headoptionCF', 'headhardwareM', 'headhardwareCF',
                            'beltM', 'beltCF', 'beltoptionCF',
                            'armsupportM', 'armsupportCF', 'armoptionM', 'armoptionCF', 'armhardwareM', 'armhardwareCF',
                            'trayM', 'trayCF', 'trayoptionM', 'trayoptionCF',
                            'lateralsupportM', 'lateralsupportCF', 'lateraloptionCF', 'lateralhardwareCF',
                            'footsupportM', 'footsupportCF', 'footoptionM', 'footoptionCF', 'foothardwareM', 'foothardwareCF',
                            'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
                            'confirmation1', 'confirmation2',
                            'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
                            'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
                            'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
                            'custom', 'costLabour']:
                    d[f'section2.section2d.{tag}'] = self._t(s2d, tag)

        # Section 3
        s3 = form.find('section3')
        if s3 is not None:
            sig = s3.find('sig')
            if sig is not None:
                for tag in ['signature', 'person', 'Date']:
                    d[f'section3.sig.{tag}'] = self._t(sig, tag)
            contact = s3.find('contact')
            if contact is not None:
                for tag in ['relationship', 'applicantLastname', 'applicantFirstname', 'applicantMiddleinitial',
                            'unitNo', 'streetNo', 'streetName', 'rrRoute',
                            'city', 'province', 'postalCode', 'homePhone', 'busPhone', 'phoneExtension']:
                    d[f'section3.contact.{tag}'] = self._t(contact, tag)

        # Section 4
        s4 = form.find('section4')
        if s4 is not None:
            auth = s4.find('authorizer')
            if auth is not None:
                for tag in ['authorizerLastname', 'authorizerFirstname', 'busPhone', 'phoneExtension', 'adpNo', 'signature', 'Date']:
                    d[f'section4.authorizer.{tag}'] = self._t(auth, tag)
            vendor = s4.find('vendor')
            if vendor is not None:
                for tag in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname', 'positionTitle', 'vendorLocation', 'busPhone', 'phoneExtension', 'signature', 'Date']:
                    d[f'section4.vendor.{tag}'] = self._t(vendor, tag)
            v2 = s4.find('vendor2')
            if v2 is not None:
                for tag in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname', 'positionTitle', 'vendorLocation', 'busPhone', 'phoneExtension', 'signature', 'Date']:
                    d[f'section4.vendor2.{tag}'] = self._t(v2, tag)
            eq = s4.find('equipmentSpec')
            if eq is not None:
                d['section4.equipmentSpec.vendorInvoiceNo'] = self._t(eq, 'vendorInvoiceNo')
                d['section4.equipmentSpec.vendorADPRegNo'] = self._t(eq, 'vendorADPRegNo')
                t2 = eq.find('Table2')
                if t2 is not None:
                    r1 = t2.find('Row1')
                    if r1 is not None:
                        for tag in ['Cell1', 'Cell2', 'Cell3', 'Cell4', 'Cell5']:
                            d[f'section4.equipmentSpec.Table2.Row1.{tag}'] = self._t(r1, tag)
            pod = s4.find('proofOfDelivery')
            if pod is not None:
                for tag in ['signature', 'receivedBy', 'Date']:
                    d[f'section4.proofOfDelivery.{tag}'] = self._t(pod, tag)
            note = s4.find('noteToADP')
            if note is not None:
                for tag in ['section1', 'section2a', 'section2b', 'section2c', 'section2d',
                            'section3and4', 'vendorReplacement', 'vendorCustom', 'fundingChart', 'letter']:
                    d[f'section4.noteToADP.{tag}'] = self._t(note, tag)

        return d

    # ------------------------------------------------------------------
    # STEP 2: JSON DICT -> MODEL FIELD VALUES
    # ------------------------------------------------------------------
    def _json_to_model_vals(self, d):
        """Map flat JSON dict to fusion.adp.application.data field values."""
        g = d.get  # shorthand
        vals = {}

        # Metadata
        vals['device_category'] = g('deviceCategory', '') or 'MD'
        vals['version_number'] = g('VersionNumber', '')

        # Section 1 - Applicant
        vals['applicant_last_name'] = g('section1.applicantLastname', '')
        vals['applicant_first_name'] = g('section1.applicantFirstname', '')
        vals['applicant_middle_initial'] = g('section1.applicantMiddleinitial', '')
        vals['health_card_number'] = g('section1.healthNo', '')
        vals['health_card_version'] = g('section1.versionNo', '')
        vals['date_of_birth'] = self._pd(g('section1.DateOfBirth', ''))
        vals['ltch_name'] = g('section1.nameLTCH', '')
        vals['unit_number'] = g('section1.unitNo', '')
        vals['street_number'] = g('section1.streetNo', '')
        vals['street_name'] = g('section1.streetName', '')
        vals['rural_route'] = g('section1.rrRoute', '')
        vals['city'] = g('section1.city', '')
        vals['province'] = g('section1.province', '')
        vals['postal_code'] = g('section1.postalCode', '')
        vals['home_phone'] = g('section1.homePhone', '')
        vals['business_phone'] = g('section1.busPhone', '')
        vals['phone_extension'] = g('section1.phoneExtension', '')

        # Benefits
        q1 = g('section1.confirmationOfBenefit.q1Yn', '').lower()
        vals['receives_social_assistance'] = q1 == 'yes'
        q1type = g('section1.confirmationOfBenefit.q1Ifyes', '').lower()
        vals['benefit_owp'] = 'owp' in q1type if q1type else False
        vals['benefit_odsp'] = 'odsp' in q1type if q1type else False
        vals['benefit_acsd'] = 'acsd' in q1type if q1type else False
        if vals['benefit_owp']:
            vals['benefit_type'] = 'owp'
        elif vals['benefit_odsp']:
            vals['benefit_type'] = 'odsp'
        elif vals['benefit_acsd']:
            vals['benefit_type'] = 'acsd'
        vals['wsib_eligible'] = g('section1.confirmationOfBenefit.q2Yn', '').lower() == 'yes'
        vals['vac_eligible'] = g('section1.confirmationOfBenefit.q3Yn', '').lower() == 'yes'

        # Section 2 - Devices & Eligibility
        vals['medical_condition'] = g('section2.devicesandEligibility.condition', '')
        vals['mobility_status'] = g('section2.devicesandEligibility.status', '')

        # Previously funded
        vals['prev_funded_none'] = bool(g('section2.devicesandEligibility.none', ''))
        vals['prev_funded_forearm'] = bool(g('section2.devicesandEligibility.forearm', ''))
        vals['prev_funded_wheeled'] = bool(g('section2.devicesandEligibility.wheeled', ''))
        vals['prev_funded_manual'] = bool(g('section2.devicesandEligibility.manual', ''))
        vals['prev_funded_power'] = bool(g('section2.devicesandEligibility.power', ''))
        vals['prev_funded_addon'] = bool(g('section2.devicesandEligibility.addOn', ''))
        vals['prev_funded_scooter'] = bool(g('section2.devicesandEligibility.scooter', ''))
        vals['prev_funded_seating'] = bool(g('section2.devicesandEligibility.seating', ''))
        vals['prev_funded_tilt'] = bool(g('section2.devicesandEligibility.tiltSystem', ''))
        vals['prev_funded_recline'] = bool(g('section2.devicesandEligibility.reclineSystem', ''))
        vals['prev_funded_legrests'] = bool(g('section2.devicesandEligibility.legRests', ''))
        vals['prev_funded_frame'] = bool(g('section2.devicesandEligibility.frame', ''))
        vals['prev_funded_stroller'] = bool(g('section2.devicesandEligibility.stroller', ''))

        # Devices currently required
        vals['device_forearm_crutches'] = bool(g('section2.devicesandEligibility.deviceForearm', ''))
        vals['device_wheeled_walker'] = bool(g('section2.devicesandEligibility.deviceWheeled', ''))
        vals['device_manual_wheelchair'] = bool(g('section2.devicesandEligibility.deviceManual', ''))
        vals['device_ambulation_manual'] = bool(g('section2.devicesandEligibility.deviceAmbulation', ''))
        vals['device_dependent_wheelchair'] = bool(g('section2.devicesandEligibility.deviceDependent', ''))
        vals['device_dynamic_tilt'] = bool(g('section2.devicesandEligibility.deviceDynamic', ''))
        vals['device_manual_dynamic'] = bool(g('section2.devicesandEligibility.manualDyanmic', ''))
        vals['device_manual_power_addon'] = bool(g('section2.devicesandEligibility.manualWheelchair', ''))
        vals['device_power_base'] = bool(g('section2.devicesandEligibility.powerBase', ''))
        vals['device_power_scooter'] = bool(g('section2.devicesandEligibility.powerScooter', ''))
        vals['device_ambulation_power'] = bool(g('section2.devicesandEligibility.ambulation', ''))
        vals['device_positioning'] = bool(g('section2.devicesandEligibility.positioning', ''))
        vals['device_high_tech'] = bool(g('section2.devicesandEligibility.highTech', ''))
        vals['device_standing_frame'] = bool(g('section2.devicesandEligibility.standingFrame', ''))
        vals['device_adp_funded_mods'] = bool(g('section2.devicesandEligibility.adpFunded', ''))
        vals['device_non_adp_funded_mods'] = bool(g('section2.devicesandEligibility.nonADPFunded', ''))

        # Section 2a - Walkers
        vals['s2a_base_device'] = g('section2.section2a.walker', '')
        vals['s2a_paediatric_frame'] = g('section2.section2a.paediatricFrame', '')
        vals['s2a_forearm_crutches'] = g('section2.section2a.forearmCrutches', '')
        vals['s2a_none'] = g('section2.section2a.none', '')
        vals['s2a_reason'] = g('section2.section2a.reason', '')
        vals['s2a_replacement_status'] = g('section2.section2a.replacementStatus', '')
        vals['s2a_replacement_size'] = g('section2.section2a.replacementSize', '')
        vals['s2a_replacement_adp'] = g('section2.section2a.replacementADP', '')
        vals['s2a_replacement_special'] = g('section2.section2a.replacementSpecial', '')
        for i in range(1, 7):
            vals[f's2a_confirm{i}'] = g(f'section2.section2a.confirmation{i}', '')
        vals['s2a_seat_height'] = g('section2.section2a.seatHeight', '')
        vals['s2a_seat_height_unit'] = g('section2.section2a.seatHeightmeasurement', '')
        vals['s2a_handle_height'] = g('section2.section2a.handleHeight', '')
        vals['s2a_handle_height_unit'] = g('section2.section2a.handleHeightmeasurement', '')
        vals['s2a_hand_grips'] = g('section2.section2a.handGrips', '')
        vals['s2a_forearm_attachments'] = g('section2.section2a.forearm', '')
        vals['s2a_width_handles'] = g('section2.section2a.widthHandles', '')
        vals['s2a_width_handles_unit'] = g('section2.section2a.widthHandlesmeasurement', '')
        vals['s2a_client_weight'] = g('section2.section2a.clientWeight', '')
        vals['s2a_client_weight_unit'] = g('section2.section2a.clientWeightmeasurement', '')
        vals['s2a_brakes'] = g('section2.section2a.brakes', '')
        vals['s2a_brake_type'] = g('section2.section2a.brakeType', '')
        vals['s2a_num_wheels'] = g('section2.section2a.noWheels', '')
        vals['s2a_wheel_size'] = g('section2.section2a.wheelSize', '')
        vals['s2a_back_support'] = g('section2.section2a.backSupport', '')
        vals['s2a_adp_walker'] = g('section2.section2a.adpWalker', '')
        vals['s2a_adp_frame'] = g('section2.section2a.adpFrame', '')
        vals['s2a_adp_standing'] = g('section2.section2a.adpStanding', '')
        vals['s2a_custom'] = g('section2.section2a.custom', '')
        vals['s2a_cost_labour'] = g('section2.section2a.costLabour', '')

        # Section 2b - Manual Wheelchairs
        vals['s2b_base_device'] = g('section2.section2b.baseDevice', '')
        vals['s2b_power_addon'] = g('section2.section2b.powerAddOndevice', '')
        vals['s2b_reason'] = g('section2.section2b.reason', '')
        vals['s2b_replacement_status'] = g('section2.section2b.replacementStatus', '')
        vals['s2b_replacement_size'] = g('section2.section2b.replacementSize', '')
        vals['s2b_replacement_adp'] = g('section2.section2b.replacementADP', '')
        vals['s2b_replacement_special'] = g('section2.section2b.replacementSpecial', '')
        for i in range(1, 14):
            vals[f's2b_confirm{i}'] = g(f'section2.section2b.confirmation{i}', '')
        vals['s2b_seat_width'] = g('section2.section2b.seatWidth', '')
        vals['s2b_seat_width_unit'] = g('section2.section2b.seatWidthmeasurement', '')
        vals['s2b_seat_depth'] = g('section2.section2b.seatDepth', '')
        vals['s2b_seat_depth_unit'] = g('section2.section2b.seatDepthmeasurement', '')
        vals['s2b_floor_height'] = g('section2.section2b.floorHeight', '')
        vals['s2b_floor_height_unit'] = g('section2.section2b.floorHeightmeasurement', '')
        vals['s2b_cane_height'] = g('section2.section2b.caneHeight', '')
        vals['s2b_cane_height_unit'] = g('section2.section2b.caneHeightmeasurement', '')
        vals['s2b_back_height'] = g('section2.section2b.backHeight', '')
        vals['s2b_back_height_unit'] = g('section2.section2b.backHeightmeasurement', '')
        vals['s2b_rest_length'] = g('section2.section2b.restLength', '')
        vals['s2b_rest_length_unit'] = g('section2.section2b.restLengthmeasurement', '')
        vals['s2b_client_weight'] = g('section2.section2b.clientWeight', '')
        vals['s2b_client_weight_unit'] = g('section2.section2b.clientWeightmeasurement', '')
        vals['s2b_adjustable_tension'] = bool(g('section2.section2b.adjustableTension', ''))
        vals['s2b_heavy_duty'] = bool(g('section2.section2b.heavyDuty', ''))
        vals['s2b_recliner'] = bool(g('section2.section2b.recliner', ''))
        vals['s2b_footplates'] = bool(g('section2.section2b.footplates', ''))
        vals['s2b_legrests'] = bool(g('section2.section2b.legrests', ''))
        vals['s2b_spoke'] = bool(g('section2.section2b.spoke', ''))
        vals['s2b_projected'] = bool(g('section2.section2b.projected', ''))
        vals['s2b_standard_manual'] = bool(g('section2.section2b.standardManual', ''))
        vals['s2b_grade_aids'] = bool(g('section2.section2b.gradeAids', ''))
        vals['s2b_caster_pin'] = bool(g('section2.section2b.casterPin', ''))
        vals['s2b_amputee_axle'] = bool(g('section2.section2b.amputeeAxle', ''))
        vals['s2b_quick_release'] = bool(g('section2.section2b.quickRelease', ''))
        vals['s2b_stroller'] = bool(g('section2.section2b.stroller', ''))
        vals['s2b_oxygen'] = bool(g('section2.section2b.oxygen', ''))
        vals['s2b_ventilator'] = bool(g('section2.section2b.ventilator', ''))
        vals['s2b_titanium'] = bool(g('section2.section2b.titanium', ''))
        vals['s2b_clothing_guards'] = bool(g('section2.section2b.clothingGuards', ''))
        vals['s2b_one_arm'] = bool(g('section2.section2b.oneArm', ''))
        vals['s2b_uni_lateral'] = bool(g('section2.section2b.uniLateral', ''))
        vals['s2b_plastic'] = bool(g('section2.section2b.plastic', ''))
        vals['s2b_rationale'] = g('section2.section2b.rationale', '')
        vals['s2b_custom'] = g('section2.section2b.custom', '')
        vals['s2b_cost_labour'] = g('section2.section2b.costLabour', '')

        # Section 2c - Power Bases / Scooters
        vals['s2c_base_device'] = g('section2.section2c.baseDevice', '')
        vals['s2c_reason'] = g('section2.section2c.reason', '')
        vals['s2c_replacement_status'] = g('section2.section2c.replacementStatus', '')
        vals['s2c_replacement_size'] = g('section2.section2c.replacementSize', '')
        vals['s2c_replacement_adp'] = g('section2.section2c.replacementADP', '')
        vals['s2c_replacement_special'] = g('section2.section2c.replacementSpecial', '')
        for i in range(1, 6):
            vals[f's2c_confirm{i}'] = g(f'section2.section2c.confirmation{i}', '')
        vals['s2c_seat_width'] = g('section2.section2c.seatWidth', '')
        vals['s2c_seat_width_unit'] = g('section2.section2c.seatWidthmeasurement', '')
        vals['s2c_back_height'] = g('section2.section2c.backHeight', '')
        vals['s2c_back_height_unit'] = g('section2.section2c.backHeightmeasurement', '')
        vals['s2c_floor_height'] = g('section2.section2c.floorHeight', '')
        vals['s2c_floor_height_unit'] = g('section2.section2c.floorHeightmeasurement', '')
        vals['s2c_rest_length'] = g('section2.section2c.restLength', '')
        vals['s2c_rest_length_unit'] = g('section2.section2c.restLengthmeasurement', '')
        vals['s2c_seat_depth'] = g('section2.section2c.seatDepth', '')
        vals['s2c_seat_depth_unit'] = g('section2.section2c.seatDepthmeasurement', '')
        vals['s2c_client_weight'] = g('section2.section2c.clientWeight', '')
        vals['s2c_client_weight_unit'] = g('section2.section2c.clientWeightmeasurement', '')
        vals['s2c_adjustable_tension'] = bool(g('section2.section2c.adjustableTension', ''))
        vals['s2c_midline'] = bool(g('section2.section2c.midline', ''))
        vals['s2c_manual_recline'] = bool(g('section2.section2c.manualRecline', ''))
        vals['s2c_footplates'] = bool(g('section2.section2c.footplates', ''))
        vals['s2c_legrests'] = bool(g('section2.section2c.legrests', ''))
        vals['s2c_swingaway'] = bool(g('section2.section2c.swingaway', ''))
        vals['s2c_one_piece'] = bool(g('section2.section2c.onePiece', ''))
        vals['s2c_seat_package_1'] = bool(g('section2.section2c.seatPackage1', ''))
        vals['s2c_seat_package_2'] = bool(g('section2.section2c.seatPackage2', ''))
        vals['s2c_oxygen'] = bool(g('section2.section2c.oxygen', ''))
        vals['s2c_ventilator'] = bool(g('section2.section2c.ventilator', ''))
        vals['s2c_sp_controls_1'] = bool(g('section2.section2c.spControls1', ''))
        vals['s2c_sp_controls_2'] = bool(g('section2.section2c.spControls2', ''))
        vals['s2c_sp_controls_3'] = bool(g('section2.section2c.spControls3', ''))
        vals['s2c_sp_controls_4'] = bool(g('section2.section2c.spControls4', ''))
        vals['s2c_sp_controls_5'] = bool(g('section2.section2c.spControls5', ''))
        vals['s2c_sp_controls_6'] = bool(g('section2.section2c.spControls6', ''))
        vals['s2c_auto_correction'] = bool(g('section2.section2c.autoCorrection', ''))
        vals['s2c_rationale'] = g('section2.section2c.rationale', '')
        vals['s2c_power_tilt'] = bool(g('section2.section2c.powerTilt', ''))
        vals['s2c_power_recline'] = bool(g('section2.section2c.powerRecline', ''))
        vals['s2c_tilt_and_recline'] = bool(g('section2.section2c.tiltAndRecline', ''))
        vals['s2c_power_elevating'] = bool(g('section2.section2c.powerElevating', ''))
        vals['s2c_control_box'] = bool(g('section2.section2c.ControlBox', ''))
        vals['s2c_custom'] = g('section2.section2c.custom', '')
        vals['s2c_cost_labour'] = g('section2.section2c.costLabour', '')

        # Section 2d - Positioning/Seating
        vals['s2d_seat_modular'] = bool(g('section2.section2d.seatM', ''))
        vals['s2d_seat_custom'] = bool(g('section2.section2d.seatCF', ''))
        vals['s2d_seat_cover_modular'] = bool(g('section2.section2d.coverM', ''))
        vals['s2d_seat_cover_custom'] = bool(g('section2.section2d.coverCF', ''))
        vals['s2d_seat_option_modular'] = bool(g('section2.section2d.optionM', ''))
        vals['s2d_seat_option_custom'] = bool(g('section2.section2d.optionCF', ''))
        vals['s2d_seat_hardware_modular'] = bool(g('section2.section2d.hardwareM', ''))
        vals['s2d_seat_hardware_custom'] = bool(g('section2.section2d.hardwareCF', ''))
        vals['s2d_adductor_modular'] = bool(g('section2.section2d.adductorM', ''))
        vals['s2d_adductor_custom'] = bool(g('section2.section2d.adductorCF', ''))
        vals['s2d_pommel_custom'] = bool(g('section2.section2d.pommelCF', ''))
        vals['s2d_back_modular'] = bool(g('section2.section2d.backM', ''))
        vals['s2d_back_custom'] = bool(g('section2.section2d.backCF', ''))
        vals['s2d_back_option_modular'] = bool(g('section2.section2d.supportoptionM', ''))
        vals['s2d_back_option_custom'] = bool(g('section2.section2d.supportoptionCF', ''))
        vals['s2d_back_cover_custom'] = bool(g('section2.section2d.backcoverCF', ''))
        vals['s2d_back_hardware_modular'] = bool(g('section2.section2d.backHardwareM', ''))
        vals['s2d_back_hardware_custom'] = bool(g('section2.section2d.backHardwareCF', ''))
        vals['s2d_complete_modular'] = bool(g('section2.section2d.completeM', ''))
        vals['s2d_complete_custom'] = bool(g('section2.section2d.completeCF', ''))
        vals['s2d_headrest_modular'] = bool(g('section2.section2d.headrestM', ''))
        vals['s2d_headrest_custom'] = bool(g('section2.section2d.headrestCF', ''))
        vals['s2d_head_option_custom'] = bool(g('section2.section2d.headoptionCF', ''))
        vals['s2d_head_hardware_modular'] = bool(g('section2.section2d.headhardwareM', ''))
        vals['s2d_head_hardware_custom'] = bool(g('section2.section2d.headhardwareCF', ''))
        vals['s2d_belt_modular'] = bool(g('section2.section2d.beltM', ''))
        vals['s2d_belt_custom'] = bool(g('section2.section2d.beltCF', ''))
        vals['s2d_belt_option_custom'] = bool(g('section2.section2d.beltoptionCF', ''))
        vals['s2d_arm_modular'] = bool(g('section2.section2d.armsupportM', ''))
        vals['s2d_arm_custom'] = bool(g('section2.section2d.armsupportCF', ''))
        vals['s2d_arm_option_modular'] = bool(g('section2.section2d.armoptionM', ''))
        vals['s2d_arm_option_custom'] = bool(g('section2.section2d.armoptionCF', ''))
        vals['s2d_arm_hardware_modular'] = bool(g('section2.section2d.armhardwareM', ''))
        vals['s2d_arm_hardware_custom'] = bool(g('section2.section2d.armhardwareCF', ''))
        vals['s2d_tray_modular'] = bool(g('section2.section2d.trayM', ''))
        vals['s2d_tray_custom'] = bool(g('section2.section2d.trayCF', ''))
        vals['s2d_tray_option_modular'] = bool(g('section2.section2d.trayoptionM', ''))
        vals['s2d_tray_option_custom'] = bool(g('section2.section2d.trayoptionCF', ''))
        vals['s2d_lateral_modular'] = bool(g('section2.section2d.lateralsupportM', ''))
        vals['s2d_lateral_custom'] = bool(g('section2.section2d.lateralsupportCF', ''))
        vals['s2d_lateral_option_custom'] = bool(g('section2.section2d.lateraloptionCF', ''))
        vals['s2d_lateral_hardware_custom'] = bool(g('section2.section2d.lateralhardwareCF', ''))
        vals['s2d_foot_modular'] = bool(g('section2.section2d.footsupportM', ''))
        vals['s2d_foot_custom'] = bool(g('section2.section2d.footsupportCF', ''))
        vals['s2d_foot_option_modular'] = bool(g('section2.section2d.footoptionM', ''))
        vals['s2d_foot_option_custom'] = bool(g('section2.section2d.footoptionCF', ''))
        vals['s2d_foot_hardware_modular'] = bool(g('section2.section2d.foothardwareM', ''))
        vals['s2d_foot_hardware_custom'] = bool(g('section2.section2d.foothardwareCF', ''))
        vals['s2d_reason'] = g('section2.section2d.reason', '')
        vals['s2d_replacement_status'] = g('section2.section2d.replacementStatus', '')
        vals['s2d_replacement_size'] = g('section2.section2d.replacementSize', '')
        vals['s2d_replacement_adp'] = g('section2.section2d.replacementADP', '')
        vals['s2d_replacement_special'] = g('section2.section2d.replacementSpecial', '')
        vals['s2d_confirm1'] = g('section2.section2d.confirmation1', '')
        vals['s2d_confirm2'] = g('section2.section2d.confirmation2', '')
        vals['s2d_custom'] = g('section2.section2d.custom', '')
        vals['s2d_cost_labour'] = g('section2.section2d.costLabour', '')

        # Section 3 - Consent
        vals['consent_date'] = self._pd(g('section3.sig.Date', ''))
        person = g('section3.sig.person', '').lower()
        vals['consent_signed_by'] = 'applicant' if 'applicant' in person else ('agent' if 'agent' in person else False)
        vals['agent_relationship'] = g('section3.contact.relationship', '')
        vals['agent_last_name'] = g('section3.contact.applicantLastname', '')
        vals['agent_first_name'] = g('section3.contact.applicantFirstname', '')
        vals['agent_middle_initial'] = g('section3.contact.applicantMiddleinitial', '')
        vals['agent_unit'] = g('section3.contact.unitNo', '')
        vals['agent_street_no'] = g('section3.contact.streetNo', '')
        vals['agent_street_name'] = g('section3.contact.streetName', '')
        vals['agent_rural_route'] = g('section3.contact.rrRoute', '')
        vals['agent_city'] = g('section3.contact.city', '')
        vals['agent_province'] = g('section3.contact.province', '')
        vals['agent_postal_code'] = g('section3.contact.postalCode', '')
        vals['agent_home_phone'] = g('section3.contact.homePhone', '')
        vals['agent_bus_phone'] = g('section3.contact.busPhone', '')
        vals['agent_phone_ext'] = g('section3.contact.phoneExtension', '')

        # Section 4 - Authorizer
        vals['authorizer_last_name'] = g('section4.authorizer.authorizerLastname', '')
        vals['authorizer_first_name'] = g('section4.authorizer.authorizerFirstname', '')
        vals['authorizer_phone'] = g('section4.authorizer.busPhone', '')
        vals['authorizer_phone_ext'] = g('section4.authorizer.phoneExtension', '')
        vals['authorizer_adp_number'] = g('section4.authorizer.adpNo', '')
        vals['assessment_date'] = self._pd(g('section4.authorizer.Date', ''))
        vals['application_date'] = vals['consent_date'] or vals['assessment_date']

        # Section 4 - Vendor 1
        vals['vendor_business_name'] = g('section4.vendor.vendorBusName', '')
        vals['vendor_adp_number'] = g('section4.vendor.adpVendorRegNo', '')
        vals['vendor_representative'] = g('section4.vendor.vendorLastfirstname', '')
        vals['vendor_position'] = g('section4.vendor.positionTitle', '')
        vals['vendor_location'] = g('section4.vendor.vendorLocation', '')
        vals['vendor_phone'] = g('section4.vendor.busPhone', '')
        vals['vendor_phone_ext'] = g('section4.vendor.phoneExtension', '')
        vals['vendor_sign_date'] = self._pd(g('section4.vendor.Date', ''))

        # Section 4 - Vendor 2
        vals['vendor2_business_name'] = g('section4.vendor2.vendorBusName', '')
        vals['vendor2_adp_number'] = g('section4.vendor2.adpVendorRegNo', '')
        vals['vendor2_representative'] = g('section4.vendor2.vendorLastfirstname', '')
        vals['vendor2_position'] = g('section4.vendor2.positionTitle', '')
        vals['vendor2_location'] = g('section4.vendor2.vendorLocation', '')
        vals['vendor2_phone'] = g('section4.vendor2.busPhone', '')
        vals['vendor2_phone_ext'] = g('section4.vendor2.phoneExtension', '')
        vals['vendor2_sign_date'] = self._pd(g('section4.vendor2.Date', ''))

        # Equipment Spec
        vals['equip_vendor_invoice_no'] = g('section4.equipmentSpec.vendorInvoiceNo', '')
        vals['equip_vendor_adp_reg'] = g('section4.equipmentSpec.vendorADPRegNo', '')
        vals['equip_cell1'] = g('section4.equipmentSpec.Table2.Row1.Cell1', '')
        vals['equip_cell2'] = g('section4.equipmentSpec.Table2.Row1.Cell2', '')
        vals['equip_cell3'] = g('section4.equipmentSpec.Table2.Row1.Cell3', '')
        vals['equip_cell4'] = g('section4.equipmentSpec.Table2.Row1.Cell4', '')
        vals['equip_cell5'] = g('section4.equipmentSpec.Table2.Row1.Cell5', '')
        vals['pod_received_by'] = g('section4.proofOfDelivery.receivedBy', '')
        vals['pod_date'] = self._pd(g('section4.proofOfDelivery.Date', ''))

        # Note to ADP
        vals['note_section1'] = bool(g('section4.noteToADP.section1', ''))
        vals['note_section2a'] = bool(g('section4.noteToADP.section2a', ''))
        vals['note_section2b'] = bool(g('section4.noteToADP.section2b', ''))
        vals['note_section2c'] = bool(g('section4.noteToADP.section2c', ''))
        vals['note_section2d'] = bool(g('section4.noteToADP.section2d', ''))
        vals['note_section3and4'] = bool(g('section4.noteToADP.section3and4', ''))
        vals['note_vendor_replacement'] = g('section4.noteToADP.vendorReplacement', '')
        vals['note_vendor_custom'] = g('section4.noteToADP.vendorCustom', '')
        vals['note_funding_chart'] = g('section4.noteToADP.fundingChart', '')
        vals['note_letter'] = g('section4.noteToADP.letter', '')

        return vals

    # ------------------------------------------------------------------
    # PROFILE MANAGEMENT
    # ------------------------------------------------------------------
    def _link_authorizer_by_adp_number(self, vals, sale_order):
        """Auto-link the authorizer contact on the sale order using the ADP number from XML."""
        adp_number = (vals.get('authorizer_adp_number') or '').strip()
        if not adp_number or adp_number.upper() in ('NA', 'N/A', ''):
            return

        if sale_order.x_fc_authorizer_id:
            return

        Partner = self.env['res.partner']
        authorizer = Partner.search([
            ('x_fc_authorizer_number', '=', adp_number),
        ], limit=1)

        if not authorizer:
            first = (vals.get('authorizer_first_name') or '').strip()
            last = (vals.get('authorizer_last_name') or '').strip()
            if first and last:
                authorizer = Partner.search([
                    '|',
                    ('name', 'ilike', f'{first} {last}'),
                    ('name', 'ilike', f'{last}, {first}'),
                ], limit=1)
                if authorizer and not authorizer.x_fc_authorizer_number:
                    authorizer.write({'x_fc_authorizer_number': adp_number})

        if authorizer:
            sale_order.write({'x_fc_authorizer_id': authorizer.id})
            _logger.info(
                'Auto-linked authorizer %s (ADP# %s) to SO %s',
                authorizer.name, adp_number, sale_order.name,
            )

    def _find_or_create_profile(self, vals, sale_order=None):
        """Find or create a client profile from parsed application data."""
        Profile = self.env['fusion.client.profile']
        hc = (vals.get('health_card_number') or '').strip()
        first = (vals.get('applicant_first_name') or '').strip()
        last = (vals.get('applicant_last_name') or '').strip()
        dob = vals.get('date_of_birth')

        profile = False
        if hc:
            profile = Profile.search([('health_card_number', '=', hc)], limit=1)
        if not profile and first and last and dob:
            profile = Profile.search([
                ('first_name', '=ilike', first),
                ('last_name', '=ilike', last),
                ('date_of_birth', '=', dob),
            ], limit=1)

        profile_vals = {
            'first_name': first,
            'last_name': last,
            'middle_initial': vals.get('applicant_middle_initial', ''),
            'health_card_number': hc,
            'health_card_version': vals.get('health_card_version', ''),
            'date_of_birth': dob,
            'ltch_name': vals.get('ltch_name', ''),
            'unit_number': vals.get('unit_number', ''),
            'street_number': vals.get('street_number', ''),
            'street_name': vals.get('street_name', ''),
            'rural_route': vals.get('rural_route', ''),
            'city': vals.get('city', ''),
            'province': vals.get('province', '') or 'ON',
            'postal_code': vals.get('postal_code', ''),
            'home_phone': vals.get('home_phone', ''),
            'business_phone': vals.get('business_phone', ''),
            'phone_extension': vals.get('phone_extension', ''),
            'medical_condition': vals.get('medical_condition', ''),
            'mobility_status': vals.get('mobility_status', ''),
        }
        if vals.get('receives_social_assistance'):
            profile_vals['receives_social_assistance'] = True
            profile_vals['benefit_type'] = vals.get('benefit_type')
        if vals.get('wsib_eligible'):
            profile_vals['wsib_eligible'] = True
        if vals.get('vac_eligible'):
            profile_vals['vac_eligible'] = True
        if vals.get('assessment_date'):
            profile_vals['last_assessment_date'] = vals['assessment_date']

        # Link to partner
        if sale_order and sale_order.partner_id:
            profile_vals['partner_id'] = sale_order.partner_id.id
        elif not profile or not profile.partner_id:
            partner = self._find_partner(first, last)
            if partner:
                profile_vals['partner_id'] = partner.id

        if profile:
            profile.write(profile_vals)
        else:
            profile = Profile.create(profile_vals)

        return profile

    def _find_partner(self, first_name, last_name):
        """Try to find a matching res.partner."""
        if not first_name or not last_name:
            return False
        Partner = self.env['res.partner']
        partner = Partner.search([('name', 'ilike', f'{first_name} {last_name}')], limit=1)
        if not partner:
            partner = Partner.search([('name', 'ilike', f'{last_name}, {first_name}')], limit=1)
        return partner or False

    # ------------------------------------------------------------------
    # HELPERS
    # ------------------------------------------------------------------
    @staticmethod
    def _t(element, tag):
        """Get text of child element, empty string if missing."""
        child = element.find(tag)
        if child is not None and child.text:
            return child.text.strip()
        return ''

    @staticmethod
    def _pd(date_str):
        """Parse date string, return date or False."""
        if not date_str:
            return False
        for fmt in ('%Y/%m/%d', '%Y-%m-%d', '%Y%m%d'):
            try:
                return datetime.strptime(date_str.strip(), fmt).date()
            except ValueError:
                continue
        return False
