# Fusion Accounting - Bank Reconciliation Report Handler
# Statement balance tracking, outstanding items, and miscellaneous ops

import logging
from datetime import date

from odoo import models, fields, _
from odoo.exceptions import UserError
from odoo.tools import SQL

_log = logging.getLogger(__name__)


class FusionBankReconciliationHandler(models.AbstractModel):
    """Custom handler for the bank reconciliation report. Computes
    last-statement balances, unreconciled items, outstanding
    payments/receipts, and miscellaneous bank journal operations."""

    _name = 'account.bank.reconciliation.report.handler'
    _inherit = 'account.report.custom.handler'
    _description = 'Bank Reconciliation Report Custom Handler'

    # ================================================================
    # OPTIONS
    # ================================================================

    def _custom_options_initializer(self, report, options, previous_options):
        super()._custom_options_initializer(report, options, previous_options=previous_options)
        options['ignore_totals_below_sections'] = True

        if 'active_id' in self.env.context and self.env.context.get('active_model') == 'account.journal':
            options['bank_reconciliation_report_journal_id'] = self.env.context['active_id']
        elif 'bank_reconciliation_report_journal_id' in previous_options:
            options['bank_reconciliation_report_journal_id'] = previous_options['bank_reconciliation_report_journal_id']
        else:
            options['bank_reconciliation_report_journal_id'] = (
                self.env['account.journal'].search([('type', '=', 'bank')], limit=1).id
            )

        has_multicur = (
            self.env.user.has_group('base.group_multi_currency')
            and self.env.user.has_group('base.group_no_one')
        )
        if not has_multicur:
            options['columns'] = [
                c for c in options['columns']
                if c['expression_label'] not in ('amount_currency', 'currency')
            ]

    # ================================================================
    # GETTERS
    # ================================================================

    def _get_bank_journal_and_currencies(self, options):
        jnl = self.env['account.journal'].browse(
            options.get('bank_reconciliation_report_journal_id'),
        )
        co_cur = jnl.company_id.currency_id
        jnl_cur = jnl.currency_id or co_cur
        return jnl, jnl_cur, co_cur

    # ================================================================
    # RESULT BUILDER
    # ================================================================

    def _build_custom_engine_result(
        self, date=None, label=None, amount_currency=None,
        amount_currency_currency_id=None, currency=None,
        amount=0, amount_currency_id=None, has_sublines=False,
    ):
        return {
            'date': date,
            'label': label,
            'amount_currency': amount_currency,
            'amount_currency_currency_id': amount_currency_currency_id,
            'currency': currency,
            'amount': amount,
            'amount_currency_id': amount_currency_id,
            'has_sublines': has_sublines,
        }

    # ================================================================
    # CUSTOM ENGINES
    # ================================================================

    def _report_custom_engine_forced_currency_amount(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        _j, jcur, _c = self._get_bank_journal_and_currencies(options)
        return self._build_custom_engine_result(amount_currency_id=jcur.id)

    def _report_custom_engine_unreconciled_last_statement_receipts(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._common_st_line_engine(options, 'receipts', current_groupby, True)

    def _report_custom_engine_unreconciled_last_statement_payments(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._common_st_line_engine(options, 'payments', current_groupby, True)

    def _report_custom_engine_unreconciled_receipts(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._common_st_line_engine(options, 'receipts', current_groupby, False)

    def _report_custom_engine_unreconciled_payments(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._common_st_line_engine(options, 'payments', current_groupby, False)

    def _report_custom_engine_outstanding_receipts(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._outstanding_engine(options, 'receipts', current_groupby)

    def _report_custom_engine_outstanding_payments(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._outstanding_engine(options, 'payments', current_groupby)

    def _report_custom_engine_misc_operations(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        report = self.env['account.report'].browse(options['report_id'])
        report._check_groupby_fields([current_groupby] if current_groupby else [])
        jnl, jcur, _c = self._get_bank_journal_and_currencies(options)
        misc_domain = self._get_bank_miscellaneous_move_lines_domain(options, jnl)
        misc_total = self.env["account.move.line"]._read_group(
            domain=misc_domain or [],
            groupby=current_groupby or [],
            aggregates=['balance:sum'],
        )[-1][0]
        return self._build_custom_engine_result(amount=misc_total or 0, amount_currency_id=jcur.id)

    def _report_custom_engine_last_statement_balance_amount(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        if current_groupby:
            raise UserError(_("Last-statement balance does not support groupby."))
        jnl, jcur, _c = self._get_bank_journal_and_currencies(options)
        last_stmt = self._get_last_bank_statement(jnl, options)
        return self._build_custom_engine_result(amount=last_stmt.balance_end_real, amount_currency_id=jcur.id)

    def _report_custom_engine_transaction_without_statement_amount(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
        return self._common_st_line_engine(options, 'all', current_groupby, False, unreconciled=False)

    # ================================================================
    # SHARED ENGINES
    # ================================================================

    def _common_st_line_engine(self, options, direction, current_groupby, from_last_stmt, unreconciled=True):
        jnl, jcur, _ccur = self._get_bank_journal_and_currencies(options)
        if not jnl:
            return self._build_custom_engine_result()

        report = self.env['account.report'].browse(options['report_id'])
        report._check_groupby_fields([current_groupby] if current_groupby else [])

        def _assemble(rows):
            if current_groupby == 'id':
                r = rows[0]
                fcur = self.env['res.currency'].browse(r['foreign_currency_id'])
                rate = (r['amount'] / r['amount_currency']) if r['amount_currency'] else 0
                return self._build_custom_engine_result(
                    date=r['date'] or None,
                    label=r['payment_ref'] or r['ref'] or '/',
                    amount_currency=-r['amount_residual'] if r['foreign_currency_id'] else None,
                    amount_currency_currency_id=fcur.id if r['foreign_currency_id'] else None,
                    currency=fcur.display_name if r['foreign_currency_id'] else None,
                    amount=-r['amount_residual'] * rate if r['amount_residual'] else None,
                    amount_currency_id=jcur.id,
                )
            total = 0
            for r in rows:
                rate = (r['amount'] / r['amount_currency']) if r['foreign_currency_id'] and r['amount_currency'] else 1
                total += -r.get('amount_residual', 0) * rate if unreconciled else r.get('amount', 0)
            return self._build_custom_engine_result(amount=total, amount_currency_id=jcur.id, has_sublines=bool(rows))

        qry = report._get_report_query(options, 'strict_range', domain=[
            ('journal_id', '=', jnl.id),
            ('account_id', '=', jnl.default_account_id.id),
        ])

        if from_last_stmt:
            last_stmt_id = self._get_last_bank_statement(jnl, options).id
            if last_stmt_id:
                stmt_cond = SQL("st_line.statement_id = %s", last_stmt_id)
            else:
                return self._compute_result([], current_groupby, _assemble)
        else:
            stmt_cond = SQL("st_line.statement_id IS NULL")

        if direction == 'receipts':
            amt_cond = SQL("AND st_line.amount > 0")
        elif direction == 'payments':
            amt_cond = SQL("AND st_line.amount < 0")
        else:
            amt_cond = SQL("")

        full_sql = SQL("""
           SELECT %(sel_gb)s,
                  st_line.id, move.name, move.ref, move.date,
                  st_line.payment_ref, st_line.amount, st_line.amount_residual,
                  st_line.amount_currency, st_line.foreign_currency_id
             FROM %(tbl)s
             JOIN account_bank_statement_line st_line ON st_line.move_id = account_move_line.move_id
             JOIN account_move move ON move.id = st_line.move_id
            WHERE %(where)s
                  %(unrec)s %(amt_cond)s
              AND %(stmt_cond)s
         GROUP BY %(gb)s, st_line.id, move.id
        """,
            sel_gb=SQL("%s AS grouping_key", SQL.identifier('account_move_line', current_groupby)) if current_groupby else SQL('null'),
            tbl=qry.from_clause,
            where=qry.where_clause,
            unrec=SQL("AND NOT st_line.is_reconciled") if unreconciled else SQL(""),
            amt_cond=amt_cond,
            stmt_cond=stmt_cond,
            gb=SQL.identifier('account_move_line', current_groupby) if current_groupby else SQL('st_line.id'),
        )
        self.env.cr.execute(full_sql)
        return self._compute_result(self.env.cr.dictfetchall(), current_groupby, _assemble)

    def _outstanding_engine(self, options, direction, current_groupby):
        jnl, jcur, ccur = self._get_bank_journal_and_currencies(options)
        if not jnl:
            return self._build_custom_engine_result()

        report = self.env['account.report'].browse(options['report_id'])
        report._check_groupby_fields([current_groupby] if current_groupby else [])

        def _assemble(rows):
            if current_groupby == 'id':
                r = rows[0]
                convert = not (jcur and r['currency_id'] == jcur.id)
                amt_cur = r['amount_residual_currency'] if r['is_account_reconcile'] else r['amount_currency']
                bal = r['amount_residual'] if r['is_account_reconcile'] else r['balance']
                fcur = self.env['res.currency'].browse(r['currency_id'])
                return self._build_custom_engine_result(
                    date=r['date'] or None,
                    label=r['ref'] or None,
                    amount_currency=amt_cur if convert else None,
                    amount_currency_currency_id=fcur.id if convert else None,
                    currency=fcur.display_name if convert else None,
                    amount=ccur._convert(bal, jcur, jnl.company_id, options['date']['date_to']) if convert else amt_cur,
                    amount_currency_id=jcur.id,
                )
            total = 0
            for r in rows:
                convert = not (jcur and r['currency_id'] == jcur.id)
                if convert:
                    bal = r['amount_residual'] if r['is_account_reconcile'] else r['balance']
                    total += ccur._convert(bal, jcur, jnl.company_id, options['date']['date_to'])
                else:
                    total += r['amount_residual_currency'] if r['is_account_reconcile'] else r['amount_currency']
            return self._build_custom_engine_result(amount=total, amount_currency_id=jcur.id, has_sublines=bool(rows))

        accts = jnl._get_journal_inbound_outstanding_payment_accounts() + jnl._get_journal_outbound_outstanding_payment_accounts()
        qry = report._get_report_query(options, 'from_beginning', domain=[
            ('journal_id', '=', jnl.id),
            ('account_id', 'in', accts.ids),
            ('full_reconcile_id', '=', False),
            ('amount_residual_currency', '!=', 0.0),
        ])

        full_sql = SQL("""
           SELECT %(sel_gb)s,
                  account_move_line.account_id, account_move_line.payment_id,
                  account_move_line.move_id, account_move_line.currency_id,
                  account_move_line.move_name AS name, account_move_line.ref,
                  account_move_line.date, account.reconcile AS is_account_reconcile,
                  SUM(account_move_line.amount_residual) AS amount_residual,
                  SUM(account_move_line.balance) AS balance,
                  SUM(account_move_line.amount_residual_currency) AS amount_residual_currency,
                  SUM(account_move_line.amount_currency) AS amount_currency
             FROM %(tbl)s
             JOIN account_account account ON account.id = account_move_line.account_id
            WHERE %(where)s AND %(dir_cond)s
         GROUP BY %(gb)s, account_move_line.account_id, account_move_line.payment_id,
                  account_move_line.move_id, account_move_line.currency_id,
                  account_move_line.move_name, account_move_line.ref,
                  account_move_line.date, account.reconcile
        """,
            sel_gb=SQL("%s AS grouping_key", SQL.identifier('account_move_line', current_groupby)) if current_groupby else SQL('null'),
            tbl=qry.from_clause,
            where=qry.where_clause,
            dir_cond=SQL("account_move_line.balance > 0") if direction == 'receipts' else SQL("account_move_line.balance < 0"),
            gb=SQL.identifier('account_move_line', current_groupby) if current_groupby else SQL('account_move_line.account_id'),
        )
        self.env.cr.execute(full_sql)
        return self._compute_result(self.env.cr.dictfetchall(), current_groupby, _assemble)

    def _compute_result(self, rows, current_groupby, builder):
        if not current_groupby:
            return builder(rows)
        grouped = {}
        for r in rows:
            grouped.setdefault(r['grouping_key'], []).append(r)
        return [(k, builder(v)) for k, v in grouped.items()]

    # ================================================================
    # POST-PROCESSING & WARNINGS
    # ================================================================

    def _custom_line_postprocessor(self, report, options, lines):
        lines = super()._custom_line_postprocessor(report, options, lines)
        jnl, _jc, _cc = self._get_bank_journal_and_currencies(options)
        if not jnl:
            return lines

        last_stmt = self._get_last_bank_statement(jnl, options)
        for ln in lines:
            line_id = report._get_res_id_from_line_id(ln['id'], 'account.report.line')
            code = self.env['account.report.line'].browse(line_id).code

            if code == "balance_bank":
                ln['name'] = _("Balance of '%s'", jnl.default_account_id.display_name)
            if code == "last_statement_balance":
                ln['class'] = 'o_bold_tr'
                if last_stmt:
                    ln['columns'][1].update({'name': last_stmt.display_name, 'auditable': True})
            if code in ("transaction_without_statement", "misc_operations"):
                ln['class'] = 'o_bold_tr'

            mdl, _mid = report._get_model_info_from_id(ln['id'])
            if mdl == "account.move.line":
                ln['name'] = ln['name'].split()[0]

        return lines

    def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
        jnl, jcur, _cc = self._get_bank_journal_and_currencies(options)
        bad_stmts = self._get_inconsistent_statements(options, jnl).ids
        misc_domain = self._get_bank_miscellaneous_move_lines_domain(options, jnl)
        has_misc = misc_domain and bool(self.env['account.move.line'].search_count(misc_domain, limit=1))
        last_stmt, gl_bal, end_bal, diff, mismatch = self._compute_journal_balances(report, options, jnl, jcur)

        if warnings is not None:
            if last_stmt and mismatch:
                warnings['fusion_accounting.journal_balance'] = {
                    'alert_type': 'warning',
                    'general_ledger_amount': gl_bal,
                    'last_bank_statement_amount': end_bal,
                    'unexplained_difference': diff,
                }
            if bad_stmts:
                warnings['fusion_accounting.inconsistent_statement_warning'] = {'alert_type': 'warning', 'args': bad_stmts}
            if has_misc:
                warnings['fusion_accounting.has_bank_miscellaneous_move_lines'] = {
                    'alert_type': 'warning',
                    'args': jnl.default_account_id.display_name,
                }

    # ================================================================
    # BALANCE COMPUTATION
    # ================================================================

    def _compute_journal_balances(self, report, options, journal, jcur):
        domain = report._get_options_domain(options, 'from_beginning')
        gl_raw = journal._get_journal_bank_account_balance(domain=domain)[0]
        last_stmt, end_raw, diff_raw, mismatch = self._compute_balances(options, journal, gl_raw, jcur)
        fmt = lambda v: report.format_value(options, v, format_params={'currency_id': jcur.id}, figure_type='monetary')
        return last_stmt, fmt(gl_raw), fmt(end_raw), fmt(diff_raw), mismatch

    def _compute_balances(self, options, journal, gl_balance, report_currency):
        rpt_date = fields.Date.from_string(options['date']['date_to'])
        last_stmt = self._get_last_bank_statement(journal, options)
        end_bal = diff = 0
        mismatch = False
        if last_stmt:
            lines_in_range = last_stmt.line_ids.filtered(lambda l: l.date <= rpt_date)
            end_bal = last_stmt.balance_start + sum(lines_in_range.mapped('amount'))
            diff = gl_balance - end_bal
            mismatch = not report_currency.is_zero(diff)
        return last_stmt, end_bal, diff, mismatch

    # ================================================================
    # STATEMENT HELPERS
    # ================================================================

    def _get_last_bank_statement(self, journal, options):
        rpt_date = fields.Date.from_string(options['date']['date_to'])
        last_line = self.env['account.bank.statement.line'].search([
            ('journal_id', '=', journal.id),
            ('statement_id', '!=', False),
            ('date', '<=', rpt_date),
        ], order='date desc, id desc', limit=1)
        return last_line.statement_id

    def _get_inconsistent_statements(self, options, journal):
        return self.env['account.bank.statement'].search([
            ('journal_id', '=', journal.id),
            ('date', '<=', options['date']['date_to']),
            ('is_valid', '=', False),
        ])

    def _get_bank_miscellaneous_move_lines_domain(self, options, journal):
        if not journal.default_account_id:
            return None
        report = self.env['account.report'].browse(options['report_id'])
        domain = [
            ('account_id', '=', journal.default_account_id.id),
            ('statement_line_id', '=', False),
            *report._get_options_domain(options, 'from_beginning'),
        ]
        lock_date = journal.company_id._get_user_fiscal_lock_date(journal)
        if lock_date != date.min:
            domain.append(('date', '>', lock_date))
        if journal.company_id.account_opening_move_id:
            domain.append(('move_id', '!=', journal.company_id.account_opening_move_id.id))
        return domain

    # ================================================================
    # AUDIT ACTIONS
    # ================================================================

    def action_audit_cell(self, options, params):
        rpt_line = self.env['account.report.line'].browse(params['report_line_id'])
        if rpt_line.code == "balance_bank":
            return self.action_redirect_to_general_ledger(options)
        elif rpt_line.code == "misc_operations":
            return self.open_bank_miscellaneous_move_lines(options)
        elif rpt_line.code == "last_statement_balance":
            return self.action_redirect_to_bank_statement_widget(options)
        return rpt_line.report_id.action_audit_cell(options, params)

    def action_redirect_to_general_ledger(self, options):
        gl_action = self.env['ir.actions.actions']._for_xml_id(
            'fusion_accounting.action_account_report_general_ledger',
        )
        gl_action['params'] = {'options': options, 'ignore_session': True}
        return gl_action

    def action_redirect_to_bank_statement_widget(self, options):
        jnl = self.env['account.journal'].browse(
            options.get('bank_reconciliation_report_journal_id'),
        )
        last_stmt = self._get_last_bank_statement(jnl, options)
        return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
            default_context={'create': False, 'search_default_statement_id': last_stmt.id},
            name=last_stmt.display_name,
        )

    def open_bank_miscellaneous_move_lines(self, options):
        jnl = self.env['account.journal'].browse(
            options['bank_reconciliation_report_journal_id'],
        )
        return {
            'name': _('Journal Items'),
            'type': 'ir.actions.act_window',
            'res_model': 'account.move.line',
            'view_type': 'list',
            'view_mode': 'list',
            'target': 'current',
            'views': [(self.env.ref('account.view_move_line_tree').id, 'list')],
            'domain': self.env['account.bank.reconciliation.report.handler']._get_bank_miscellaneous_move_lines_domain(options, jnl),
        }

    def bank_reconciliation_report_open_inconsistent_statements(self, options, params=None):
        stmt_ids = params['args']
        action = {
            'name': _("Inconsistent Statements"),
            'type': 'ir.actions.act_window',
            'res_model': 'account.bank.statement',
        }
        if len(stmt_ids) == 1:
            action.update({'view_mode': 'form', 'res_id': stmt_ids[0], 'views': [(False, 'form')]})
        else:
            action.update({'view_mode': 'list', 'domain': [('id', 'in', stmt_ids)], 'views': [(False, 'list')]})
        return action
