# Fusion Accounting - Cash Basis Reporting
# Copyright (C) 2026 Nexa Systems Inc. (https://nexasystems.ca)
# Original implementation for the Fusion Accounting module.
#
# Alternative report handler that uses payment dates instead of invoice
# dates for recognizing revenue and expenses, supporting the cash basis
# accounting method.

import logging

from odoo import api, fields, models, _
from odoo.tools import SQL, Query, float_is_zero

_logger = logging.getLogger(__name__)


class FusionCashBasisReport(models.AbstractModel):
    """Cash basis report custom handler.

    Unlike the standard accrual-based reporting, cash basis reports
    recognise revenue when payment is received and expenses when payment
    is made, regardless of when the invoice was issued.

    This handler:
      - Replaces the invoice/bill date with the payment reconciliation date
      - Filters transactions to only include those with matching payments
      - Provides a toggle in report options to switch between accrual and
        cash basis views
    """

    _name = 'account.cash.basis.report.handler'
    _inherit = 'account.report.custom.handler'
    _description = 'Cash Basis Report Custom Handler'

    # ------------------------------------------------------------------
    # Options Initializer
    # ------------------------------------------------------------------

    def _custom_options_initializer(self, report, options, previous_options):
        """Add cash-basis specific options to the report."""
        super()._custom_options_initializer(report, options, previous_options=previous_options)

        # Add the cash basis toggle
        options['fusion_cash_basis'] = previous_options.get('fusion_cash_basis', True)

        # Restrict to journals that support cash basis
        report._init_options_journals(
            options,
            previous_options=previous_options,
            additional_journals_domain=[('type', 'in', ('sale', 'purchase', 'bank', 'cash', 'general'))],
        )

    # ------------------------------------------------------------------
    # Dynamic Lines
    # ------------------------------------------------------------------

    def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
        """Generate report lines based on cash basis (payment date) accounting.

        Returns a list of (sequence, line_dict) tuples for the report engine.
        """
        output_lines = []

        if not options.get('fusion_cash_basis', True):
            # Fallback to standard accrual-based processing
            return output_lines

        cash_data = self._compute_cash_basis_data(report, options)

        # ---- Revenue Section ----
        revenue_total = sum(d['amount'] for d in cash_data.get('revenue', []))
        output_lines.append((0, self._build_section_line(
            report, options, 'revenue',
            _("Cash Revenue"), revenue_total,
        )))
        for entry in sorted(cash_data.get('revenue', []), key=lambda e: e.get('date', '')):
            output_lines.append((1, self._build_detail_line(report, options, entry)))

        # ---- Expense Section ----
        expense_total = sum(d['amount'] for d in cash_data.get('expense', []))
        output_lines.append((0, self._build_section_line(
            report, options, 'expense',
            _("Cash Expenses"), expense_total,
        )))
        for entry in sorted(cash_data.get('expense', []), key=lambda e: e.get('date', '')):
            output_lines.append((1, self._build_detail_line(report, options, entry)))

        # ---- Net Cash Income ----
        net_total = revenue_total - abs(expense_total)
        output_lines.append((0, self._build_section_line(
            report, options, 'net_income',
            _("Net Cash Income"), net_total,
        )))

        return output_lines

    # ------------------------------------------------------------------
    # Data Computation
    # ------------------------------------------------------------------

    def _compute_cash_basis_data(self, report, options):
        """Compute cash basis amounts grouped by revenue/expense.

        Queries reconciled payments to find the actual cash dates for
        recognised amounts.

        :returns: dict with keys ``revenue`` and ``expense``, each
                  containing a list of entry dicts with amount, date,
                  account, and partner information.
        """
        result = {'revenue': [], 'expense': []}
        company_ids = [c['id'] for c in options.get('companies', [{'id': self.env.company.id}])]
        date_from = options.get('date', {}).get('date_from')
        date_to = options.get('date', {}).get('date_to')

        if not date_from or not date_to:
            return result

        # Query: find all payment reconciliation entries within the period
        query = """
            SELECT
                aml.id AS line_id,
                aml.account_id,
                aa.name AS account_name,
                aa.code AS account_code,
                aml.partner_id,
                rp.name AS partner_name,
                apr.max_date AS cash_date,
                CASE
                    WHEN aa.account_type IN ('income', 'income_other')
                    THEN aml.credit - aml.debit
                    ELSE aml.debit - aml.credit
                END AS amount
            FROM account_move_line aml
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN res_partner rp ON rp.id = aml.partner_id
            JOIN account_move am ON am.id = aml.move_id
            JOIN (
                SELECT
                    apr2.debit_move_id,
                    apr2.credit_move_id,
                    apr2.max_date
                FROM account_partial_reconcile apr2
                WHERE apr2.max_date >= %s
                  AND apr2.max_date <= %s
            ) apr ON (apr.debit_move_id = aml.id OR apr.credit_move_id = aml.id)
            WHERE am.state = 'posted'
              AND am.company_id IN %s
              AND aa.account_type IN (
                  'income', 'income_other',
                  'expense', 'expense_direct_cost', 'expense_depreciation'
              )
            ORDER BY apr.max_date, aa.code
        """

        self.env.cr.execute(query, (date_from, date_to, tuple(company_ids)))
        rows = self.env.cr.dictfetchall()

        seen_lines = set()
        for row in rows:
            # Avoid counting the same line twice if partially reconciled
            if row['line_id'] in seen_lines:
                continue
            seen_lines.add(row['line_id'])

            entry = {
                'line_id': row['line_id'],
                'account_id': row['account_id'],
                'account_name': row['account_name'],
                'account_code': row['account_code'] or '',
                'partner_id': row['partner_id'],
                'partner_name': row['partner_name'] or '',
                'date': str(row['cash_date']),
                'amount': row['amount'] or 0.0,
            }

            account_type = self.env['account.account'].browse(
                row['account_id']
            ).account_type
            if account_type in ('income', 'income_other'):
                result['revenue'].append(entry)
            else:
                result['expense'].append(entry)

        return result

    # ------------------------------------------------------------------
    # Line Builders
    # ------------------------------------------------------------------

    def _build_section_line(self, report, options, section_id, name, total):
        """Build a section header line for the report.

        :param section_id: unique identifier for the section
        :param name: display name of the section
        :param total: aggregated monetary total
        :returns: line dict compatible with the report engine
        """
        columns = report._build_column_dict(total, options, figure_type='monetary')
        return {
            'id': report._get_generic_line_id(None, None, markup=f'fusion_cb_{section_id}'),
            'name': name,
            'level': 1,
            'columns': [columns],
            'unfoldable': False,
            'unfolded': False,
        }

    def _build_detail_line(self, report, options, entry):
        """Build a detail line for a single cash-basis entry.

        :param entry: dict with amount, account_code, account_name, etc.
        :returns: line dict compatible with the report engine
        """
        name = f"{entry['account_code']} {entry['account_name']}"
        if entry.get('partner_name'):
            name += f" - {entry['partner_name']}"

        columns = report._build_column_dict(
            entry['amount'], options, figure_type='monetary',
        )

        return {
            'id': report._get_generic_line_id(
                'account.move.line', entry['line_id'],
                markup='fusion_cb_detail',
            ),
            'name': name,
            'level': 3,
            'columns': [columns],
            'caret_options': 'account.move.line',
            'unfoldable': False,
        }
