# Fusion Accounting - CSV Bank Statement Import
# Extends the base import wizard to handle bank statement CSV files
# with debit/credit columns, running balances, and date ordering

import contextlib

import psycopg2

from odoo import _, api, fields, models, Command
from odoo.exceptions import UserError, ValidationError
from odoo.addons.base_import.models.base_import import FIELDS_RECURSION_LIMIT


class AccountBankStmtImportCSV(models.TransientModel):
    """Extends the standard CSV import to support bank-statement-specific
    columns such as debit, credit, and cumulative balance, automatically
    computing net amounts and statement boundaries."""

    _inherit = 'base_import.import'

    @api.model
    def get_fields_tree(self, model, depth=FIELDS_RECURSION_LIMIT):
        """Append virtual monetary columns (balance, debit, credit)
        when the import is running in bank statement context."""
        available_fields = super().get_fields_tree(model, depth=depth)

        if self.env.context.get('bank_stmt_import', False):
            extra_columns = [
                {
                    'id': 'balance',
                    'name': 'balance',
                    'string': 'Cumulative Balance',
                    'required': False,
                    'fields': [],
                    'type': 'monetary',
                    'model_name': model,
                },
                {
                    'id': 'debit',
                    'name': 'debit',
                    'string': 'Debit',
                    'required': False,
                    'fields': [],
                    'type': 'monetary',
                    'model_name': model,
                },
                {
                    'id': 'credit',
                    'name': 'credit',
                    'string': 'Credit',
                    'required': False,
                    'fields': [],
                    'type': 'monetary',
                    'model_name': model,
                },
            ]
            available_fields.extend(extra_columns)

        return available_fields

    def _safe_float(self, raw_value):
        """Safely convert a string or empty value to float, defaulting to 0.0."""
        return float(raw_value) if raw_value else 0.0

    def _parse_import_data(self, data, import_fields, options):
        # EXTENDS base
        data = super()._parse_import_data(data, import_fields, options)

        target_journal_id = self.env.context.get('default_journal_id')
        is_bank_import = options.get('bank_stmt_import')
        if not target_journal_id or not is_bank_import:
            return data

        # Validate that either amount OR both debit and credit are mapped
        amount_mapped = 'amount' in import_fields
        credit_mapped = 'credit' in import_fields
        debit_mapped = 'debit' in import_fields

        if (debit_mapped ^ credit_mapped) or not (amount_mapped ^ debit_mapped):
            raise ValidationError(
                _("Make sure that an Amount or Debit and Credit is in the file.")
            )

        stmt_metadata = options['statement_vals'] = {}
        output_rows = []

        import_fields.append('sequence')
        balance_col_idx = False
        need_amount_conversion = False

        # Ensure rows are sorted chronologically (ascending or descending accepted)
        if 'date' in import_fields:
            date_col = import_fields.index('date')
            parsed_dates = [
                fields.Date.from_string(row[date_col])
                for row in data
                if row[date_col]
            ]
            ascending_order = sorted(parsed_dates)
            if parsed_dates != ascending_order:
                descending_order = ascending_order[::-1]
                if parsed_dates == descending_order:
                    # Flip to ascending for consistent processing
                    data = data[::-1]
                else:
                    raise UserError(_('Rows must be sorted by date.'))

        # Handle debit/credit column conversion to a single amount
        if 'debit' in import_fields and 'credit' in import_fields:
            debit_col = import_fields.index('debit')
            credit_col = import_fields.index('credit')
            self._parse_float_from_data(data, debit_col, 'debit', options)
            self._parse_float_from_data(data, credit_col, 'credit', options)
            import_fields.append('amount')
            need_amount_conversion = True

        # Extract opening and closing balance from the balance column
        if 'balance' in import_fields:
            balance_col_idx = import_fields.index('balance')
            self._parse_float_from_data(data, balance_col_idx, 'balance', options)

            first_row_balance = self._safe_float(data[0][balance_col_idx])
            amount_col = import_fields.index('amount')

            if not need_amount_conversion:
                first_row_amount = self._safe_float(data[0][amount_col])
            else:
                first_row_amount = (
                    abs(self._safe_float(data[0][credit_col]))
                    - abs(self._safe_float(data[0][debit_col]))
                )

            stmt_metadata['balance_start'] = first_row_balance - first_row_amount
            stmt_metadata['balance_end_real'] = data[-1][balance_col_idx]
            import_fields.remove('balance')

        # Clean up temporary column mappings
        if need_amount_conversion:
            import_fields.remove('debit')
            import_fields.remove('credit')

        # Build final row data with sequence numbers, converting debit/credit
        for seq_num, row in enumerate(data):
            row.append(seq_num)
            cols_to_drop = []

            if need_amount_conversion:
                net_amount = (
                    abs(self._safe_float(row[credit_col]))
                    - abs(self._safe_float(row[debit_col]))
                )
                row.append(net_amount)
                cols_to_drop.extend([debit_col, credit_col])

            if balance_col_idx:
                cols_to_drop.append(balance_col_idx)

            # Drop virtual columns in reverse order to preserve indices
            for drop_idx in sorted(cols_to_drop, reverse=True):
                del row[drop_idx]

            # Only include rows that have a non-zero amount
            if row[import_fields.index('amount')]:
                output_rows.append(row)

        return output_rows

    def parse_preview(self, options, count=10):
        """Inject bank statement context flag when previewing CSV data."""
        if options.get('bank_stmt_import', False):
            self = self.with_context(bank_stmt_import=True)
        return super().parse_preview(options, count=count)

    def execute_import(self, fields, columns, options, dryrun=False):
        """Execute the import, wrapping bank statement rows into a
        statement record with computed balance boundaries."""
        if options.get('bank_stmt_import'):
            with self.env.cr.savepoint(flush=False) as sp:
                import_result = super().execute_import(fields, columns, options, dryrun=dryrun)

                if 'statement_id' not in fields:
                    new_statement = self.env['account.bank.statement'].create({
                        'reference': self.file_name,
                        'line_ids': [Command.set(import_result.get('ids', []))],
                        **options.get('statement_vals', {}),
                    })
                    if not dryrun:
                        import_result['messages'].append({
                            'statement_id': new_statement.id,
                            'type': 'bank_statement',
                        })

                with contextlib.suppress(psycopg2.InternalError):
                    sp.close(rollback=dryrun)

                return import_result
        else:
            return super().execute_import(fields, columns, options, dryrun=dryrun)
