import re

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


class RepairOrder(models.Model):
    _inherit = 'repair.order'

    # =========================
    # RELACIÓN PRINCIPAL ERP
    # =========================

    sale_order_id = fields.Many2one(
        'sale.order',
        string='Presupuesto Principal',
        required=False
    )

    sale_order_ids = fields.Many2many(
        'sale.order',
        'repair_order_sale_order_rel',
        'repair_order_id',
        'sale_order_id',
        string='Presupuestos',
        required=False
    )

    # =========================
    # COMPRAS
    # =========================

    purchase_order_ids = fields.Many2many(
        'purchase.order',
        'repair_order_purchase_order_rel',
        'repair_order_id',
        'purchase_order_id',
        string='Pedidos de Compra',
        required=False
    )

    # =========================
    # ALBARANES
    # =========================
    # repair.order estándar de Odoo 18 tiene 'picking_id' (singular) pero NO 'picking_ids'.

    picking_ids = fields.One2many(
        'stock.picking',
        'repair_order_id',
        string='Albaranes',
        required=False
    )

    # =========================
    # FACTURAS (derivadas de los presupuestos relacionados)
    # =========================

    invoice_ids = fields.Many2many(
        'account.move',
        compute='_compute_invoice_ids',
        string='Facturas',
        required=False
    )

    # =========================
    # MÁQUINA DEL CLIENTE
    # =========================

    relsum_machine_id = fields.Many2one(
        'relsum.machine',
        string='Máquina',
        ondelete='restrict',
        index=True,
        tracking=True,
        help='Máquina del cliente. Permite trazabilidad histórica entre OTs.'
    )

    # Datos derivados de la máquina (sólo display)
    machine_marca = fields.Char(related='relsum_machine_id.marca', readonly=True, string='Marca (de la máquina)')
    machine_tipo = fields.Char(related='relsum_machine_id.tipo', readonly=True, string='Tipo (de la máquina)')
    machine_numero_serie = fields.Char(related='relsum_machine_id.numero_serie', readonly=True, string='Nº serie (de la máquina)')
    machine_kva = fields.Float(related='relsum_machine_id.kva', readonly=True, string='kVA (de la máquina)')
    machine_voltios = fields.Float(related='relsum_machine_id.voltios', readonly=True, string='Voltios (de la máquina)')
    machine_rpm = fields.Float(related='relsum_machine_id.rpm', readonly=True, string='RPM (de la máquina)')
    machine_amperios = fields.Float(related='relsum_machine_id.amperios', readonly=True, string='Amperios (de la máquina)')
    machine_hz = fields.Float(related='relsum_machine_id.hz', readonly=True, string='Hz (de la máquina)')
    machine_planta_id = fields.Many2one('res.partner', related='relsum_machine_id.planta_id', readonly=True, string='Planta (de la máquina)')

    # =========================
    # WORKFLOW RELSUM (estados alineados con el procedimiento de la empresa)
    # =========================

    relsum_workflow_stage = fields.Selection(
        selection=[
            ('received', 'Recibida'),
            ('diagnostic', 'En diagnóstico'),
            ('quoted', 'Presupuestada'),
            ('in_progress', 'En reparación'),
            ('ready', 'Lista para entrega'),
            ('delivered', 'Entregada'),
            ('invoiced', 'Facturada'),
        ],
        string='Fase',
        default='received',
        tracking=True,
        copy=False,
        help='Fase del procedimiento RELSUM. Avanza con los botones del encabezado.',
    )

    # =========================
    # CUENTA ANALÍTICA Y TIMESHEETS
    # =========================

    analytic_account_id = fields.Many2one(
        'account.analytic.account',
        string='Cuenta analítica',
        copy=False,
        groups='analytic.group_analytic_accounting',
        help='Se crea automáticamente al crear la OT. '
             'Sólo visible para usuarios del grupo "Contabilidad analítica".'
    )

    timesheet_ids = fields.One2many(
        'account.analytic.line',
        'repair_order_id',
        string='Partes de trabajo',
    )
    timesheet_hours = fields.Float(
        string='Horas totales',
        compute='_compute_timesheet_hours',
    )

    @api.depends('timesheet_ids.unit_amount')
    def _compute_timesheet_hours(self):
        for rec in self:
            rec.timesheet_hours = sum(rec.timesheet_ids.mapped('unit_amount'))

    # =========================
    # INFORMES TÉCNICOS Y CAJA RH
    # =========================

    technical_report_ids = fields.One2many(
        'relsum.technical.report',
        'repair_order_id',
        string='Informes técnicos',
    )

    petty_cash_ref_ids = fields.One2many(
        'relsum.petty.cash.ref',
        'repair_order_id',
        string='Cajas RH',
    )

    # =========================
    # CAMPOS LEGACY (mantenidos en BD para no perder los datos importados)
    # =========================

    x_numero_ot = fields.Integer(string='Nº OT (legacy)', index=True, copy=False)
    x_presupuesto = fields.Char(string='Nº Presupuesto', index=True)
    x_numero_pedido = fields.Char(string='Nº Pedido Cliente')
    x_marca = fields.Char(string='Marca')
    x_tipo_maquina = fields.Char(string='Tipo / Modelo')
    x_numero_serie = fields.Char(string='Nº Serie', index=True)
    x_planta = fields.Char(string='Planta')
    x_voltios = fields.Float(string='Voltios (V)', digits=(10, 2))
    x_kva = fields.Float(string='Potencia (kVA)', digits=(10, 2))
    x_rpm = fields.Float(string='RPM', digits=(10, 0))
    x_amperios = fields.Float(string='Amperios (A)', digits=(10, 2))
    x_hz = fields.Float(string='Frecuencia (Hz)', digits=(10, 2))
    x_albaran = fields.Char(string='Nº Albarán (texto legacy)')
    x_albaran_nota = fields.Char(string='Nota Albarán')
    x_pedido_proveedor = fields.Char(string='Pedido a Proveedores (texto legacy)')
    x_comentarios = fields.Text(string='Comentarios')

    # =========================
    # CONTADORES (smart buttons)
    # =========================

    sale_order_count = fields.Integer(compute='_compute_sale_order_count', string='Nº de presupuestos')
    purchase_order_count = fields.Integer(compute='_compute_purchase_order_count', string='Nº de compras')
    picking_count = fields.Integer(compute='_compute_picking_count', string='Nº de albaranes')
    technical_report_count = fields.Integer(compute='_compute_technical_report_count', string='Nº de informes')
    petty_cash_ref_count = fields.Integer(compute='_compute_petty_cash_ref_count', string='Nº de cajas')
    invoice_count = fields.Integer(compute='_compute_invoice_ids', string='Nº de facturas')

    @api.depends('sale_order_ids')
    def _compute_sale_order_count(self):
        for rec in self:
            rec.sale_order_count = len(rec.sale_order_ids)

    @api.depends('purchase_order_ids')
    def _compute_purchase_order_count(self):
        for rec in self:
            rec.purchase_order_count = len(rec.purchase_order_ids)

    @api.depends('picking_ids')
    def _compute_picking_count(self):
        for rec in self:
            rec.picking_count = len(rec.picking_ids)

    @api.depends('technical_report_ids')
    def _compute_technical_report_count(self):
        for rec in self:
            rec.technical_report_count = len(rec.technical_report_ids)

    @api.depends('petty_cash_ref_ids')
    def _compute_petty_cash_ref_count(self):
        for rec in self:
            rec.petty_cash_ref_count = len(rec.petty_cash_ref_ids)

    @api.depends('sale_order_ids.invoice_ids', 'sale_order_id.invoice_ids')
    def _compute_invoice_ids(self):
        for rec in self:
            moves = rec.sale_order_ids.mapped('invoice_ids')
            if rec.sale_order_id:
                moves |= rec.sale_order_id.invoice_ids
            moves = moves.filtered(lambda m: m.move_type in ('out_invoice', 'out_refund'))
            rec.invoice_ids = moves
            rec.invoice_count = len(moves)

    # =========================
    # SMART BUTTON: VER LISTAS RELACIONADAS
    # =========================

    def action_view_sale_orders(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Presupuestos',
            'view_mode': 'list,form',
            'res_model': 'sale.order',
            'domain': [('id', 'in', self.sale_order_ids.ids)],
            'context': {
                'default_partner_id': self.partner_id.id,
                'default_repair_order_id': self.id,
            },
        }

    def action_view_purchase_orders(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Pedidos a Proveedor',
            'view_mode': 'list,form',
            'res_model': 'purchase.order',
            'domain': [('id', 'in', self.purchase_order_ids.ids)],
        }

    def action_view_pickings(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Albaranes',
            'view_mode': 'list,form',
            'res_model': 'stock.picking',
            'domain': [('id', 'in', self.picking_ids.ids)],
        }

    def action_view_technical_reports(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Informes técnicos',
            'view_mode': 'list,form',
            'res_model': 'relsum.technical.report',
            'domain': [('id', 'in', self.technical_report_ids.ids)],
            'context': {'default_repair_order_id': self.id},
        }

    def action_view_petty_cash(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Cajas RH',
            'view_mode': 'list,form',
            'res_model': 'relsum.petty.cash.ref',
            'domain': [('id', 'in', self.petty_cash_ref_ids.ids)],
            'context': {'default_repair_order_id': self.id},
        }

    def action_view_invoices(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Facturas',
            'view_mode': 'list,form',
            'res_model': 'account.move',
            'domain': [('id', 'in', self.invoice_ids.ids)],
        }

    # =========================
    # ACCIONES "CREAR X DESDE LA OT" (atajos para el empleado)
    # =========================

    def action_create_machine(self):
        """Crea una máquina pre-rellenada con los datos legacy de la OT
        y la asocia al campo relsum_machine_id. Si ya existe, abre la
        existente."""
        self.ensure_one()
        if self.relsum_machine_id:
            return self._open_record('relsum.machine', self.relsum_machine_id.id)

        machine = self.env['relsum.machine'].create({
            'partner_id': self.partner_id.id,
            'marca': self.x_marca or False,
            'tipo': self.x_tipo_maquina or False,
            'numero_serie': self.x_numero_serie or False,
            'voltios': self.x_voltios or 0,
            'kva': self.x_kva or 0,
            'rpm': self.x_rpm or 0,
            'amperios': self.x_amperios or 0,
            'hz': self.x_hz or 0,
        })
        self.relsum_machine_id = machine.id
        return self._open_record('relsum.machine', machine.id)

    def action_create_sale(self):
        """Crea un sale.order pre-rellenado con cliente + OT y lo abre."""
        self.ensure_one()
        sale = self.env['sale.order'].create({
            'partner_id': self.partner_id.id,
            'repair_order_id': self.id,
        })
        self.sale_order_ids = [(4, sale.id)]
        if not self.sale_order_id:
            self.sale_order_id = sale.id
        # Auto-avance de fase
        if self.relsum_workflow_stage in ('received', 'diagnostic'):
            self.relsum_workflow_stage = 'quoted'
        return self._open_record('sale.order', sale.id)

    def action_create_purchase(self):
        """Abre el formulario de purchase.order con la OT pre-vinculada."""
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Nuevo pedido a proveedor',
            'res_model': 'purchase.order',
            'view_mode': 'form',
            'target': 'current',
            'context': {
                'default_repair_order_ids': [(6, 0, [self.id])],
            },
        }

    def action_create_picking(self):
        """Abre formulario de stock.picking outgoing pre-rellenado."""
        self.ensure_one()
        pt_out = self.env['stock.picking.type'].search([
            ('code', '=', 'outgoing'),
            ('warehouse_id.company_id', '=', self.env.company.id),
        ], limit=1) or self.env['stock.picking.type'].search([('code', '=', 'outgoing')], limit=1)
        return {
            'type': 'ir.actions.act_window',
            'name': 'Nuevo albarán',
            'res_model': 'stock.picking',
            'view_mode': 'form',
            'target': 'current',
            'context': {
                'default_partner_id': self.partner_id.id,
                'default_picking_type_id': pt_out.id if pt_out else False,
                'default_repair_order_id': self.id,
                'default_tipo_albaran': 'repair',
            },
        }

    def action_create_invoice(self):
        """Genera factura desde el presupuesto confirmado vinculado a la OT."""
        self.ensure_one()
        sale = self.sale_order_id
        if not sale:
            confirmed = self.sale_order_ids.filtered(lambda s: s.state == 'sale')
            sale = confirmed[:1]
        if not sale:
            raise UserError('Esta OT no tiene presupuesto vinculado.')
        if sale.state != 'sale':
            raise UserError(
                'El presupuesto %s no está confirmado (estado=%s). '
                'Confírmalo primero.' % (sale.name, sale.state)
            )
        invoices = sale._create_invoices()
        if not invoices:
            raise UserError('No se generaron facturas (¿ya estaba todo facturado?).')
        # Auto-avance de fase
        if self.relsum_workflow_stage in ('ready', 'delivered'):
            self.relsum_workflow_stage = 'invoiced'
        return self._open_record('account.move', invoices[0].id)

    def _open_record(self, model, res_id):
        return {
            'type': 'ir.actions.act_window',
            'res_model': model,
            'view_mode': 'form',
            'res_id': res_id,
            'target': 'current',
        }

    # =========================
    # TRANSICIONES DE FASE (botones del encabezado)
    # =========================

    def action_stage_received(self):
        self.write({'relsum_workflow_stage': 'received'})

    def action_stage_diagnostic(self):
        self.write({'relsum_workflow_stage': 'diagnostic'})

    def action_stage_quoted(self):
        self.write({'relsum_workflow_stage': 'quoted'})

    def action_stage_in_progress(self):
        self.write({'relsum_workflow_stage': 'in_progress'})

    def action_stage_ready(self):
        self.write({'relsum_workflow_stage': 'ready'})

    def action_stage_delivered(self):
        self.write({'relsum_workflow_stage': 'delivered'})

    def action_stage_invoiced(self):
        self.write({'relsum_workflow_stage': 'invoiced'})

    # =========================
    # CREATE OVERRIDE: secuencia OT-NNNNN + cuenta analítica auto
    # =========================

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if not vals.get('name') or vals.get('name') == '/':
                seq = self.env['ir.sequence'].next_by_code('repair.order.ot')
                if seq:
                    vals['name'] = seq
        records = super().create(vals_list)
        # Auto cuenta analítica si no se especificó.
        # SE USA sudo() porque los usuarios normales (sin grupo
        # analytic.group_analytic_accounting) no pueden leer account.analytic.plan
        # ni crear account.analytic.account — pero la cuenta analítica de la OT
        # debe crearse silenciosamente al crear cualquier OT, sin requerir
        # permisos administrativos al operario que abre la OT.
        plan = self.env['account.analytic.plan'].sudo().search([], limit=1)
        if plan:
            AA = self.env['account.analytic.account'].sudo()
            for r in records:
                if not r.sudo().analytic_account_id:
                    aa_vals = {
                        'name': '%s - %s' % (
                            r.name or '(OT sin nombre)',
                            r.partner_id.name or 'sin cliente',
                        ),
                        'plan_id': plan.id,
                    }
                    if r.partner_id:
                        aa_vals['partner_id'] = r.partner_id.id
                    aa = AA.create(aa_vals)
                    r.sudo().write({'analytic_account_id': aa.id})
        return records

    # =========================
    # UNLINK: liberar el nº correlativo si se borra la última OT
    # =========================

    def _ot_sequence_number(self):
        """Nº correlativo si el name sigue el patrón de la secuencia (OT-NNNNN).
        Devuelve int o False (las OTs legacy 'WH/RO/...' no cuentan)."""
        self.ensure_one()
        if self.name:
            m = re.match(r'^OT-0*(\d+)$', self.name.strip())
            if m:
                return int(m.group(1))
        return False

    def unlink(self):
        # nº emitidos por la secuencia entre los registros que se van a borrar
        deleted_nums = {
            n for n in (rec._ot_sequence_number() for rec in self) if n
        }
        res = super().unlink()
        if deleted_nums:
            seq = self.env['ir.sequence'].sudo().search(
                [('code', '=', 'repair.order.ot')], limit=1
            )
            if seq:
                # number_next_actual es computado y puede quedar cacheado; lo
                # invalidamos para leer el valor real de la secuencia PostgreSQL.
                seq.invalidate_recordset(['number_next_actual'])
                next_actual = seq.number_next_actual
                # Recorre hacia atrás desde el último nº emitido (next-1) mientras
                # cada nº consecutivo esté entre los borrados: esos quedan libres.
                # Si se borra una OT que NO es la última, el bucle no entra y la
                # secuencia no cambia (sólo retrocede, nunca avanza).
                rollback_to = next_actual
                cur = next_actual - 1
                while cur in deleted_nums:
                    rollback_to = cur
                    cur -= 1
                if rollback_to < next_actual:
                    seq.number_next_actual = rollback_to
        return res
