# -*- coding: utf-8 -*-

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


class WheelchairConfigFlow(models.Model):
    _name = 'fusion.wc.config.flow'
    _description = 'Wheelchair Configuration Flow'
    _order = 'sequence, id'

    name = fields.Char(string='Name', required=True,
        help='e.g. "Standard Manual Wheelchair Config"')
    active = fields.Boolean(default=True)
    sequence = fields.Integer(default=10)

    equipment_type = fields.Selection(
        selection='_get_equipment_type_selection',
        string='Equipment Type', required=True)

    @api.model
    def _get_equipment_type_selection(self):
        types = self.env['fusion.equipment.type'].sudo().search([], order='sequence')
        if types:
            return [(t.code, t.name) for t in types]
        return [
            ('manual_wheelchair', 'Manual Wheelchair'),
            ('power_wheelchair', 'Power Wheelchair'),
            ('walker', 'Walker / Ambulation Aid'),
        ]

    description = fields.Text(string='Description')

    # Canvas state — JSON blob for viewport (zoom, pan) preserved across sessions
    canvas_data = fields.Text(string='Canvas Data', default='{}',
        help='JSON: viewport state for the visual designer')

    state = fields.Selection([
        ('draft', 'Draft'),
        ('active', 'Active'),
        ('archived', 'Archived'),
    ], string='Status', default='draft', tracking=True)

    # Relationships
    node_ids = fields.One2many('fusion.wc.config.flow.node', 'flow_id',
        string='Nodes')
    connection_ids = fields.One2many('fusion.wc.config.flow.connection', 'flow_id',
        string='Connections')
    step_ids = fields.One2many('fusion.wc.config.flow.step', 'flow_id',
        string='Form Steps')

    # Computed counts
    node_count = fields.Integer(string='Nodes', compute='_compute_counts')
    connection_count = fields.Integer(string='Connections', compute='_compute_counts')
    step_count = fields.Integer(string='Steps', compute='_compute_counts')

    @api.depends('node_ids', 'connection_ids', 'step_ids')
    def _compute_counts(self):
        for rec in self:
            rec.node_count = len(rec.node_ids)
            rec.connection_count = len(rec.connection_ids)
            rec.step_count = len(rec.step_ids)

    # ------------------------------------------------------------------
    # Actions
    # ------------------------------------------------------------------
    def action_open_designer(self):
        """Open the visual flow designer (OWL client action)."""
        self.ensure_one()
        return {
            'type': 'ir.actions.client',
            'tag': 'fusion_flow_designer',
            'name': _('Flow Designer: %s') % self.name,
            'context': {'active_id': self.id},
        }

    def action_activate(self):
        """Set flow to active. Deactivate other flows for same equipment type.
        Auto-creates default steps if flow has no steps defined.
        """
        self.ensure_one()
        # Auto-create default steps if none exist
        if not self.step_ids:
            self.action_create_default_steps()
        # Deactivate other active flows for the same equipment type
        siblings = self.search([
            ('equipment_type', '=', self.equipment_type),
            ('state', '=', 'active'),
            ('id', '!=', self.id),
        ])
        siblings.write({'state': 'draft'})
        self.write({'state': 'active'})

    def action_archive(self):
        self.ensure_one()
        self.write({'state': 'archived'})

    def action_reset_draft(self):
        self.ensure_one()
        self.write({'state': 'draft'})

    def action_create_default_steps(self):
        """Generate default form steps based on equipment type.
        For wheelchair types, creates the standard 6 steps.
        For other types, creates a generic 4-step flow.
        """
        self.ensure_one()
        Step = self.env['fusion.wc.config.flow.step']

        # Remove existing steps
        self.step_ids.unlink()

        wheelchair_types = ('manual_wheelchair', 'power_wheelchair')
        if self.equipment_type in wheelchair_types:
            # ── Client step config: ADP program fields ──
            is_manual = self.equipment_type == 'manual_wheelchair'
            client_config = json.dumps({
                "show_health_card": True,
                "show_dob": True,
                "show_adp_fields": True,        # client_type + reason_for_application
                "show_wheelchair_category": is_manual,
                "show_powerchair_category": not is_manual,
            })
            # ── Measurement fields ──
            mw_measurements_json = json.dumps([
                {"name": "seat_width", "label": "Seat Width", "type": "float",
                 "unit_field": "seat_width_unit", "units": ["inches", "cm"], "required": True},
                {"name": "seat_depth", "label": "Seat Depth", "type": "float",
                 "unit_field": "seat_depth_unit", "units": ["inches", "cm"], "required": True},
                {"name": "finished_seat_to_floor_height", "label": "Finished Seat to Floor Height",
                 "type": "float", "unit_field": "seat_to_floor_unit", "units": ["inches", "cm"]},
                {"name": "back_cane_height", "label": "Back Cane Height", "type": "float",
                 "unit_field": "cane_height_unit", "units": ["inches", "cm"]},
                {"name": "finished_back_height", "label": "Finished Back Height", "type": "float",
                 "unit_field": "back_height_unit", "units": ["inches", "cm"]},
                {"name": "finished_leg_rest_length", "label": "Finished Leg Rest Length",
                 "type": "float", "unit_field": "leg_rest_unit", "units": ["inches", "cm"]},
                {"name": "client_weight", "label": "Client Weight", "type": "float",
                 "unit_field": "client_weight_unit", "units": ["lbs", "kg"], "required": True},
            ])
            prefix = 'mw' if is_manual else 'pw'
            steps = [
                {'sequence': 10, 'name': 'Client', 'step_type': 'client_info',
                 'icon': 'fa-user', 'fields_json': client_config},
                {'sequence': 20, 'name': 'Measurements', 'step_type': 'measurements',
                 'icon': 'fa-ruler', 'fields_json': mw_measurements_json},
                {'sequence': 30, 'name': 'Frame', 'step_type': 'product_select',
                 'icon': 'fa-wheelchair', 'section_code': f'{prefix}_frame'},
                {'sequence': 40, 'name': 'Seating', 'step_type': 'options',
                 'icon': 'fa-chair', 'section_code': 'seating'},
                {'sequence': 50, 'name': 'Options', 'step_type': 'options',
                 'icon': 'fa-list', 'section_code': f'{prefix}_adp_options,{prefix}_accessories'},
                {'sequence': 60, 'name': 'Review', 'step_type': 'review',
                 'icon': 'fa-check'},
            ]
        elif self.equipment_type == 'walker':
            # ── Client step config: ADP program fields (no wheelchair categories) ──
            client_config = json.dumps({
                "show_health_card": True,
                "show_dob": True,
                "show_adp_fields": True,
            })
            walker_measurements_json = json.dumps([
                {"name": "walker_seat_height", "label": "Seat Height", "type": "float",
                 "unit_field": "walker_seat_height_unit", "units": ["inches", "cm", "na"]},
                {"name": "push_handle_height", "label": "Push Handle Height", "type": "float",
                 "unit_field": "push_handle_height_unit", "units": ["inches", "cm"]},
                {"name": "width_between_push_handles", "label": "Width Between Push Handles",
                 "type": "float", "unit_field": "push_handle_width_unit", "units": ["inches", "cm"]},
                {"name": "client_weight", "label": "Client Weight", "type": "float",
                 "unit_field": "client_weight_unit", "units": ["lbs", "kg"], "required": True},
            ])
            steps = [
                {'sequence': 10, 'name': 'Client', 'step_type': 'client_info',
                 'icon': 'fa-user', 'fields_json': client_config},
                {'sequence': 20, 'name': 'Measurements', 'step_type': 'measurements',
                 'icon': 'fa-ruler', 'fields_json': walker_measurements_json},
                {'sequence': 30, 'name': 'Equipment', 'step_type': 'product_select',
                 'icon': 'fa-male', 'section_code': 'walker_frame'},
                {'sequence': 40, 'name': 'Options', 'step_type': 'options',
                 'icon': 'fa-list', 'section_code': 'walker_adp_options,walker_accessories'},
                {'sequence': 50, 'name': 'Review', 'step_type': 'review',
                 'icon': 'fa-check'},
            ]
        else:
            # ── Generic flow for new equipment types (stair lift, porch lift, etc.) ──
            # No ADP fields, no health card, no DOB — pure quotation tool
            steps = [
                {'sequence': 10, 'name': 'Client', 'step_type': 'client_info',
                 'icon': 'fa-user'},  # no fields_json → no optional groups
                {'sequence': 20, 'name': 'Equipment', 'step_type': 'product_select',
                 'icon': 'fa-cog'},
                {'sequence': 30, 'name': 'Options', 'step_type': 'options',
                 'icon': 'fa-list'},
                {'sequence': 40, 'name': 'Review', 'step_type': 'review',
                 'icon': 'fa-check'},
            ]

        for step_vals in steps:
            step_vals['flow_id'] = self.id
            Step.create(step_vals)

    def action_new_assessment_form(self):
        """Open a new portal assessment form pre-selected to this flow's equipment type."""
        self.ensure_one()
        base = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
        return {
            'type': 'ir.actions.act_url',
            'url': f'{base}/my/quotation/builder/new?equipment_type={self.equipment_type}',
            'target': 'new',
        }

    # ------------------------------------------------------------------
    # Load / Save graph for designer
    # ------------------------------------------------------------------
    def load_flow_graph(self):
        """Return the complete flow graph for the visual designer."""
        self.ensure_one()
        nodes = []
        for n in self.node_ids:
            node_data = {
                'id': n.id,
                'name': n.name,
                'node_type': n.node_type,
                'pos_x': n.pos_x,
                'pos_y': n.pos_y,
                'color': n.color,
                'icon': n.icon,
                'section_id': n.section_id.id if n.section_id else False,
                'section_name': n.section_id.name if n.section_id else '',
                'decision_field': n.decision_field or '',
                'decision_operator': n.decision_operator or '',
                'decision_value': n.decision_value or '',
                'measurement_field': n.measurement_field or '',
                'comparison': n.comparison or '',
                'threshold_value': n.threshold_value,
                'action_type': n.action_type or '',
                'target_option_ids': n.target_option_ids.ids,
                'target_step': n.target_step,
                'config_json': n.config_json or '{}',
                'node_options': [{
                    'id': opt.id,
                    'name': opt.name,
                    'sequence': opt.sequence,
                    'section_option_id': opt.section_option_id.id if opt.section_option_id else False,
                    'enables_option_ids': opt.enables_option_ids.ids,
                    'disables_option_ids': opt.disables_option_ids.ids,
                    'requires_option_ids': opt.requires_option_ids.ids,
                    'port_key': opt.port_key,
                } for opt in n.node_option_ids],
            }
            nodes.append(node_data)

        connections = []
        for c in self.connection_ids:
            connections.append({
                'id': c.id,
                'source_node_id': c.source_node_id.id,
                'target_node_id': c.target_node_id.id,
                'source_port': c.source_port or 'out',
                'label': c.label or '',
                'condition_json': c.condition_json or '{}',
                'sequence': c.sequence,
            })

        return {
            'id': self.id,
            'name': self.name,
            'equipment_type': self.equipment_type,
            'canvas': json.loads(self.canvas_data or '{}'),
            'nodes': nodes,
            'connections': connections,
        }

    def save_flow_graph(self, graph_data):
        """Sync the full graph from the visual designer to ORM records."""
        self.ensure_one()
        Node = self.env['fusion.wc.config.flow.node']
        Connection = self.env['fusion.wc.config.flow.connection']

        # Save viewport state
        canvas = graph_data.get('canvas', {})
        self.write({'canvas_data': json.dumps(canvas)})

        # --- Sync Nodes ---
        incoming_nodes = graph_data.get('nodes', [])
        incoming_node_ids = set()
        node_id_map = {}  # temp_id -> real_id

        for ndata in incoming_nodes:
            vals = {
                'flow_id': self.id,
                'name': ndata.get('name', 'Untitled'),
                'node_type': ndata.get('node_type', 'action'),
                'pos_x': ndata.get('pos_x', 0),
                'pos_y': ndata.get('pos_y', 0),
                'color': ndata.get('color', '#3b82f6'),
                'icon': ndata.get('icon', 'fa-circle'),
                'section_id': ndata.get('section_id') or False,
                'decision_field': ndata.get('decision_field') or False,
                'decision_operator': ndata.get('decision_operator') or False,
                'decision_value': ndata.get('decision_value') or '',
                'measurement_field': ndata.get('measurement_field') or False,
                'comparison': ndata.get('comparison') or False,
                'threshold_value': ndata.get('threshold_value', 0),
                'action_type': ndata.get('action_type') or False,
                'target_step': ndata.get('target_step', 0),
                'config_json': ndata.get('config_json', '{}'),
            }
            target_ids = ndata.get('target_option_ids', [])

            node_id = ndata.get('id')
            if isinstance(node_id, int) and node_id > 0:
                # Update existing
                node = Node.browse(node_id)
                if node.exists():
                    node.write(vals)
                    if target_ids is not None:
                        node.write({'target_option_ids': [(6, 0, target_ids)]})
                    node_id_map[node_id] = node_id
                    incoming_node_ids.add(node_id)
                    continue
            # Create new
            new_node = Node.create(vals)
            if target_ids:
                new_node.write({'target_option_ids': [(6, 0, target_ids)]})
            node_id_map[ndata.get('id', 'new')] = new_node.id
            incoming_node_ids.add(new_node.id)

        # Delete nodes no longer in the graph
        existing_nodes = Node.search([('flow_id', '=', self.id)])
        to_delete = existing_nodes.filtered(lambda n: n.id not in incoming_node_ids)
        to_delete.unlink()

        # --- Sync Connections ---
        incoming_conns = graph_data.get('connections', [])
        incoming_conn_ids = set()

        for cdata in incoming_conns:
            src_id = cdata.get('source_node_id')
            tgt_id = cdata.get('target_node_id')
            # Resolve temp IDs
            src_id = node_id_map.get(src_id, src_id)
            tgt_id = node_id_map.get(tgt_id, tgt_id)

            vals = {
                'flow_id': self.id,
                'source_node_id': src_id,
                'target_node_id': tgt_id,
                'source_port': cdata.get('source_port', 'out'),
                'label': cdata.get('label', ''),
                'condition_json': cdata.get('condition_json', '{}'),
                'sequence': cdata.get('sequence', 10),
            }

            conn_id = cdata.get('id')
            if isinstance(conn_id, int) and conn_id > 0:
                conn = Connection.browse(conn_id)
                if conn.exists():
                    conn.write(vals)
                    incoming_conn_ids.add(conn_id)
                    continue
            new_conn = Connection.create(vals)
            incoming_conn_ids.add(new_conn.id)

        # Delete connections no longer in the graph
        existing_conns = Connection.search([('flow_id', '=', self.id)])
        to_delete = existing_conns.filtered(lambda c: c.id not in incoming_conn_ids)
        to_delete.unlink()

        return True

    # ------------------------------------------------------------------
    # Flow Evaluation Engine
    # ------------------------------------------------------------------
    def evaluate(self, assessment_data):
        """Walk the flow graph and return option directives.

        Args:
            assessment_data: dict with keys like equipment_type, seat_width,
                selected_option_ids, build_type, etc.

        Returns:
            dict with enabled/disabled/required option IDs, skip steps, messages.
        """
        self.ensure_one()
        result = {
            'enabled_option_ids': set(),
            'disabled_option_ids': set(),
            'required_option_ids': set(),
            'skip_steps': set(),
            'active_nodes': [],
            'messages': [],
        }

        # Find start node
        start_nodes = self.node_ids.filtered(lambda n: n.node_type == 'start')
        if not start_nodes:
            return self._finalize_result(result)

        # BFS walk with cycle protection
        visited = set()
        queue = list(start_nodes)
        max_iterations = 200

        iteration = 0
        while queue and iteration < max_iterations:
            iteration += 1
            node = queue.pop(0)
            if node.id in visited:
                continue
            visited.add(node.id)
            result['active_nodes'].append(node.id)

            next_nodes = self._evaluate_node(node, assessment_data, result)
            queue.extend(next_nodes)

        return self._finalize_result(result)

    def _finalize_result(self, result):
        """Convert sets to sorted lists for JSON serialization."""
        for key in ('enabled_option_ids', 'disabled_option_ids',
                     'required_option_ids', 'skip_steps'):
            result[key] = sorted(result[key])
        return result

    def _evaluate_node(self, node, data, result):
        """Evaluate a single node. Returns list of next nodes to visit."""
        if node.node_type == 'start':
            return self._get_next_nodes(node, 'out')

        elif node.node_type == 'end':
            return []

        elif node.node_type == 'decision':
            passed = self._evaluate_decision(node, data)
            return self._get_next_nodes(node, 'true' if passed else 'false')

        elif node.node_type == 'measurement_check':
            passed = self._evaluate_measurement(node, data)
            return self._get_next_nodes(node, 'pass' if passed else 'fail')

        elif node.node_type == 'option_group':
            selected = set(data.get('selected_option_ids', []))
            for opt in node.node_option_ids:
                if opt.section_option_id and opt.section_option_id.id in selected:
                    result['enabled_option_ids'].update(opt.enables_option_ids.ids)
                    result['disabled_option_ids'].update(opt.disables_option_ids.ids)
                    result['required_option_ids'].update(opt.requires_option_ids.ids)
            return self._get_next_nodes(node, 'out')

        elif node.node_type == 'action':
            if node.action_type == 'enable':
                result['enabled_option_ids'].update(node.target_option_ids.ids)
            elif node.action_type == 'disable':
                result['disabled_option_ids'].update(node.target_option_ids.ids)
            elif node.action_type == 'require':
                result['required_option_ids'].update(node.target_option_ids.ids)
            elif node.action_type == 'skip_step' and node.target_step:
                result['skip_steps'].add(node.target_step)
            elif node.action_type == 'set_value':
                msg = json.loads(node.config_json or '{}').get('message', '')
                if msg:
                    result['messages'].append(msg)
            return self._get_next_nodes(node, 'out')

        elif node.node_type == 'product_select':
            return self._get_next_nodes(node, 'out')

        return []

    def _evaluate_decision(self, node, data):
        """Evaluate a decision node condition against assessment data."""
        field = node.decision_field
        op = node.decision_operator
        expected = node.decision_value or ''

        actual = data.get(field, '')
        if isinstance(actual, (int, float)) and expected:
            try:
                expected_num = float(expected)
                actual_num = float(actual)
                if op == 'eq':
                    return actual_num == expected_num
                elif op == 'neq':
                    return actual_num != expected_num
                elif op == 'gt':
                    return actual_num > expected_num
                elif op == 'gte':
                    return actual_num >= expected_num
                elif op == 'lt':
                    return actual_num < expected_num
                elif op == 'lte':
                    return actual_num <= expected_num
            except (ValueError, TypeError):
                pass

        # String comparison
        actual_str = str(actual)
        if op == 'eq':
            return actual_str == expected
        elif op == 'neq':
            return actual_str != expected
        elif op == 'in':
            return actual_str in [v.strip() for v in expected.split(',')]
        return False

    def _evaluate_measurement(self, node, data):
        """Evaluate a measurement check node."""
        field = node.measurement_field
        if not field:
            return False
        actual = data.get(field, 0)
        try:
            actual = float(actual)
        except (ValueError, TypeError):
            return False

        threshold = node.threshold_value
        comp = node.comparison
        if comp == 'gt':
            return actual > threshold
        elif comp == 'gte':
            return actual >= threshold
        elif comp == 'lt':
            return actual < threshold
        elif comp == 'eq':
            return abs(actual - threshold) < 0.001
        elif comp == 'neq':
            return abs(actual - threshold) >= 0.001
        return False

    def _get_next_nodes(self, node, port):
        """Get target nodes for outgoing connections from a specific port."""
        connections = node.outgoing_connection_ids.filtered(
            lambda c: c.source_port == port
        )
        return connections.mapped('target_node_id')
