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


class LaundrySession(models.Model):
    """Daily operational session — POS-style cash management.

    A session groups all laundry orders placed in a single working shift.
    Cash totals are derived from standard account.payment records linked
    to the session's invoices, so figures are always in sync with Accounting.

    Opening balance:
    - When opening a new session, the system suggests the previous
      session's actual_closing_cash as the opening float (carry-forward).

    Closing:
    - Staff enter the actual physical cash count.
    - If a difference exists, a journal entry is posted (optional).
    """
    _name = 'laundry.session'
    _description = 'Laundry Daily Session'
    _inherit = ['mail.thread']
    _order = 'opening_datetime desc, id desc'

    # ── Identity ──────────────────────────────────────────────────────
    name = fields.Char(
        string='Session',
        required=True, copy=False, readonly=True,
        default='New', tracking=True,
    )
    user_id = fields.Many2one(
        'res.users', string='Opened By',
        required=True, default=lambda self: self.env.user,
        tracking=True,
    )
    company_id = fields.Many2one(
        'res.company',
        default=lambda self: self.env.company,
    )

    # ── State ─────────────────────────────────────────────────────────
    state = fields.Selection([
        ('new',    'New'),
        ('opened', 'Open'),
        ('closed', 'Closed'),
    ], default='new', required=True, tracking=True, copy=False)

    # ── Timing ────────────────────────────────────────────────────────
    opening_datetime = fields.Datetime(string='Opened At', readonly=True)
    closing_datetime = fields.Datetime(string='Closed At', readonly=True)

    # ── Cash Control ──────────────────────────────────────────────────
    opening_cash = fields.Float(
        string='Opening Float',
        digits=(10, 2),
        help='Cash in the drawer at the start of the session.',
    )
    actual_closing_cash = fields.Float(
        string='Actual Cash Count',
        digits=(10, 2),
        help='Physical cash counted at session close.',
    )
    expected_closing_cash = fields.Float(
        string='Expected Cash',
        compute='_compute_cash_control', store=True, digits=(10, 2),
    )
    cash_difference = fields.Float(
        string='Difference',
        compute='_compute_cash_control', store=True, digits=(10, 2),
        help='Actual − Expected. Negative = short.',
    )

    # ── Orders (sale.order with session_id = self) ────────────────────
    order_ids = fields.One2many(
        'sale.order', 'session_id', string='Orders',
    )
    order_count = fields.Integer(
        compute='_compute_session_totals', store=True,
    )
    total_sales = fields.Float(
        string='Total Sales',
        compute='_compute_session_totals', store=True, digits=(10, 2),
    )

    # ── Payment totals (from account.payment via invoices) ────────────
    total_cash = fields.Float(
        string='Cash',
        compute='_compute_session_totals', store=True, digits=(10, 2),
    )
    total_bank = fields.Float(
        string='Bank / Card',
        compute='_compute_session_totals', store=True, digits=(10, 2),
    )
    total_paid = fields.Float(
        string='Total Collected',
        compute='_compute_session_totals', store=True, digits=(10, 2),
    )
    total_credit = fields.Float(
        string='Outstanding / Deferred',
        compute='_compute_session_totals', store=True, digits=(10, 2),
        help='Total not yet paid (total_sales − total_paid).',
    )
    outstanding_amount = fields.Float(
        string='Outstanding',
        compute='_compute_session_totals', store=True, digits=(10, 2),
    )

    notes = fields.Text(string='Notes')

    # ── Difference account ────────────────────────────────────────────
    difference_account_id = fields.Many2one(
        'account.account',
        string='Cash Difference Account',
        help='Account used to post cash variances at session close.',
    )

    # ── Constraints ───────────────────────────────────────────────────
    _session_name_uniq = models.Constraint(
        'UNIQUE(name)',
        'Session name must be unique.',
    )

    @api.constrains('state', 'company_id')
    def _check_single_open_session(self):
        for session in self:
            if session.state == 'opened':
                duplicate = self.search([
                    ('state',      '=', 'opened'),
                    ('company_id', '=', session.company_id.id),
                    ('id',         '!=', session.id),
                ], limit=1)
                if duplicate:
                    raise UserError(
                        f'Session "{duplicate.name}" is already open.\n'
                        'Close it before opening a new session.'
                    )

    # ── Computed ──────────────────────────────────────────────────────
    @api.depends(
        'order_ids.amount_total',
        'order_ids.state',
        'order_ids.invoice_ids.state',
        'order_ids.invoice_ids.payment_state',
        'order_ids.invoice_ids.amount_residual',
        'order_ids.is_laundry_order',
    )
    def _compute_session_totals(self):
        Payment = self.env['account.payment']
        for session in self:
            active = session.order_ids.filtered(
                lambda o: o.is_laundry_order and o.state not in ('cancel', 'draft')
            )
            session.order_count = len(active)
            session.total_sales = sum(active.mapped('amount_total'))

            # Get posted customer invoices for active orders
            invoices = active.mapped('invoice_ids').filtered(
                lambda inv: inv.state == 'posted' and inv.move_type == 'out_invoice'
            )

            if not invoices:
                session.total_cash        = 0.0
                session.total_bank        = 0.0
                session.total_paid        = 0.0
                session.outstanding_amount = session.total_sales
                session.total_credit      = session.total_sales
                continue

            # Payments reconciled against those invoices
            # account.payment.reconciled_invoice_ids is a computed M2M in Odoo 16+
            payments = Payment.search([
                ('reconciled_invoice_ids', 'in', invoices.ids),
                ('state',                 '=',  'posted'),
                ('payment_type',          '=',  'inbound'),
            ])

            cash_pmts = payments.filtered(lambda p: p.journal_id.type == 'cash')
            bank_pmts = payments.filtered(lambda p: p.journal_id.type == 'bank')

            session.total_cash   = sum(cash_pmts.mapped('amount'))
            session.total_bank   = sum(bank_pmts.mapped('amount'))
            session.total_paid   = sum(payments.mapped('amount'))
            outstanding          = max(session.total_sales - session.total_paid, 0.0)
            session.outstanding_amount = outstanding
            session.total_credit = outstanding

    @api.depends('opening_cash', 'total_cash', 'actual_closing_cash', 'state')
    def _compute_cash_control(self):
        for session in self:
            session.expected_closing_cash = (
                session.opening_cash + session.total_cash
            )
            session.cash_difference = (
                session.actual_closing_cash - session.expected_closing_cash
                if session.state == 'closed' else 0.0
            )

    # ── ORM ───────────────────────────────────────────────────────────
    @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('laundry.session')
                    or 'New'
                )
        return super().create(vals_list)

    # ── Workflow ──────────────────────────────────────────────────────
    def action_open_session(self):
        for session in self:
            if session.state != 'new':
                raise UserError('This session is already open or closed.')
            # Carry forward: suggest previous session's actual closing cash
            if not session.opening_cash:
                prev = self.search([
                    ('state',      '=', 'closed'),
                    ('company_id', '=', session.company_id.id),
                ], order='closing_datetime desc', limit=1)
                if prev and prev.actual_closing_cash:
                    session.opening_cash = prev.actual_closing_cash
            session.write({
                'state':            'opened',
                'opening_datetime': fields.Datetime.now(),
            })
            session.message_post(
                body=f'Session opened by {self.env.user.name}. '
                     f'Opening float: {session.opening_cash:.2f}'
            )

    def action_close_session(self):
        self.ensure_one()
        if self.state != 'opened':
            raise UserError('Only open sessions can be closed.')
        return {
            'name':      'Close Session',
            'type':      'ir.actions.act_window',
            'res_model': 'laundry.session.close.wizard',
            'view_mode': 'form',
            'target':    'new',
            'context':   {'default_session_id': self.id},
        }

    def action_view_orders(self):
        self.ensure_one()
        return {
            'name':      f'Orders — {self.name}',
            'type':      'ir.actions.act_window',
            'res_model': 'sale.order',
            'view_mode': 'list,form',
            'domain':    [('session_id', '=', self.id), ('is_laundry_order', '=', True)],
            'context':   {'default_session_id': self.id, 'default_is_laundry_order': True},
        }

    def action_print_session_report(self):
        self.ensure_one()
        return self.env.ref(
            'laundry_management.action_report_laundry_session'
        ).report_action(self)
