# Fusion Accounting - Lock Date Management Wizard
# Provides UI for managing fiscal, tax, sales, and purchase lock dates
# with support for temporary exceptions and draft entry warnings.

from datetime import date, timedelta

from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import date_utils

from odoo.addons.account.models.company import SOFT_LOCK_DATE_FIELDS, LOCK_DATE_FIELDS


class AccountChangeLockDate(models.TransientModel):
    """Wizard that enables administrators to update accounting lock dates
    and manage exceptions for individual users or the entire organization."""

    _name = 'account.change.lock.date'
    _description = 'Change Lock Date'

    # --- Core Company Reference ---
    company_id = fields.Many2one(
        comodel_name='res.company',
        required=True,
        readonly=True,
        default=lambda self: self.env.company,
    )

    # --- Fiscal Year Lock ---
    fiscalyear_lock_date = fields.Date(
        string='Lock Everything',
        default=lambda self: self.env.company.fiscalyear_lock_date,
        help="Entries on or before this date are locked and will be "
             "rescheduled according to the journal's sequence.",
    )
    fiscalyear_lock_date_for_me = fields.Date(
        string='Lock Everything For Me',
        compute='_compute_lock_date_exceptions',
    )
    fiscalyear_lock_date_for_everyone = fields.Date(
        string='Lock Everything For Everyone',
        compute='_compute_lock_date_exceptions',
    )
    min_fiscalyear_lock_date_exception_for_me_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )
    min_fiscalyear_lock_date_exception_for_everyone_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )

    # --- Tax Lock ---
    tax_lock_date = fields.Date(
        string="Lock Tax Return",
        default=lambda self: self.env.company.tax_lock_date,
        help="Tax entries on or before this date are locked. This date "
             "updates automatically when a tax closing entry is posted.",
    )
    tax_lock_date_for_me = fields.Date(
        string='Lock Tax Return For Me',
        compute='_compute_lock_date_exceptions',
    )
    tax_lock_date_for_everyone = fields.Date(
        string='Lock Tax Return For Everyone',
        compute='_compute_lock_date_exceptions',
    )
    min_tax_lock_date_exception_for_me_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )
    min_tax_lock_date_exception_for_everyone_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )

    # --- Sales Lock ---
    sale_lock_date = fields.Date(
        string='Lock Sales',
        default=lambda self: self.env.company.sale_lock_date,
        help="Sales entries on or before this date are locked and will "
             "be postponed per the journal's sequence.",
    )
    sale_lock_date_for_me = fields.Date(
        string='Lock Sales For Me',
        compute='_compute_lock_date_exceptions',
    )
    sale_lock_date_for_everyone = fields.Date(
        string='Lock Sales For Everyone',
        compute='_compute_lock_date_exceptions',
    )
    min_sale_lock_date_exception_for_me_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )
    min_sale_lock_date_exception_for_everyone_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )

    # --- Purchase Lock ---
    purchase_lock_date = fields.Date(
        string='Lock Purchases',
        default=lambda self: self.env.company.purchase_lock_date,
        help="Purchase entries on or before this date are locked and "
             "will be postponed per the journal's sequence.",
    )
    purchase_lock_date_for_me = fields.Date(
        string='Lock Purchases For Me',
        compute='_compute_lock_date_exceptions',
    )
    purchase_lock_date_for_everyone = fields.Date(
        string='Lock Purchases For Everyone',
        compute='_compute_lock_date_exceptions',
    )
    min_purchase_lock_date_exception_for_me_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )
    min_purchase_lock_date_exception_for_everyone_id = fields.Many2one(
        comodel_name='account.lock_exception',
        compute='_compute_lock_date_exceptions',
    )

    # --- Hard (Irreversible) Lock ---
    hard_lock_date = fields.Date(
        string='Hard Lock',
        default=lambda self: self.env.company.hard_lock_date,
        help="Entries on or before this date are permanently locked. "
             "This lock cannot be removed and allows no exceptions.",
    )
    current_hard_lock_date = fields.Date(
        string='Current Hard Lock',
        related='company_id.hard_lock_date',
        readonly=True,
    )

    # --- Exception Configuration ---
    exception_needed = fields.Boolean(
        string='Exception needed',
        compute='_compute_exception_needed',
    )
    exception_needed_fields = fields.Char(
        compute='_compute_exception_needed_fields',
    )
    exception_applies_to = fields.Selection(
        string='Exception applies',
        selection=[
            ('me', "for me"),
            ('everyone', "for everyone"),
        ],
        default='me',
        required=True,
    )
    exception_duration = fields.Selection(
        string='Exception Duration',
        selection=[
            ('5min', "for 5 minutes"),
            ('15min', "for 15 minutes"),
            ('1h', "for 1 hour"),
            ('24h', "for 24 hours"),
            ('forever', "forever"),
        ],
        default='5min',
        required=True,
    )
    exception_reason = fields.Char(
        string='Exception Reason',
    )

    # --- Warning for draft entries ---
    show_draft_entries_warning = fields.Boolean(
        string="Show Draft Entries Warning",
        compute='_compute_show_draft_entries_warning',
    )

    # -------------------------------------------------------------------------
    # Compute Methods
    # -------------------------------------------------------------------------

    @api.depends('company_id')
    @api.depends_context('user', 'company')
    def _compute_lock_date_exceptions(self):
        """Retrieve active lock date exceptions for each soft lock field
        and determine the minimum exception per user scope."""
        LockException = self.env['account.lock_exception']
        for wiz in self:
            active_domain = LockException._get_active_exceptions_domain(
                wiz.company_id, SOFT_LOCK_DATE_FIELDS,
            )
            active_exceptions = LockException.search(active_domain)
            current_uid = self.env.user.id

            for lock_field in SOFT_LOCK_DATE_FIELDS:
                # Partition exceptions by scope (current user vs global)
                my_exceptions = active_exceptions.filtered(
                    lambda exc: exc.lock_date_field == lock_field and exc.user_id.id == current_uid
                )
                global_exceptions = active_exceptions.filtered(
                    lambda exc: exc.lock_date_field == lock_field and not exc.user_id.id
                )

                # Find the earliest exception in each scope
                earliest_mine = (
                    min(my_exceptions, key=lambda e: e[lock_field] or date.min)
                    if my_exceptions else False
                )
                earliest_global = (
                    min(global_exceptions, key=lambda e: e[lock_field] or date.min)
                    if global_exceptions else False
                )

                wiz[f"min_{lock_field}_exception_for_me_id"] = earliest_mine
                wiz[f"min_{lock_field}_exception_for_everyone_id"] = earliest_global
                wiz[f"{lock_field}_for_me"] = earliest_mine.lock_date if earliest_mine else False
                wiz[f"{lock_field}_for_everyone"] = earliest_global.lock_date if earliest_global else False

    def _build_draft_moves_domain(self):
        """Build a domain to find draft moves that would fall within
        any of the configured lock periods."""
        self.ensure_one()
        period_domains = []
        if self.hard_lock_date:
            period_domains.append([('date', '<=', self.hard_lock_date)])
        if self.fiscalyear_lock_date:
            period_domains.append([('date', '<=', self.fiscalyear_lock_date)])
        if self.sale_lock_date:
            period_domains.append([
                ('date', '<=', self.sale_lock_date),
                ('journal_id.type', '=', 'sale'),
            ])
        if self.purchase_lock_date:
            period_domains.append([
                ('date', '<=', self.purchase_lock_date),
                ('journal_id.type', '=', 'purchase'),
            ])
        return [
            ('company_id', 'child_of', self.env.company.id),
            ('state', '=', 'draft'),
            *expression.OR(period_domains),
        ]

    # Keep backward-compatible alias
    _get_draft_moves_in_locked_period_domain = _build_draft_moves_domain

    @api.depends('fiscalyear_lock_date', 'tax_lock_date', 'sale_lock_date',
                 'purchase_lock_date', 'hard_lock_date')
    def _compute_show_draft_entries_warning(self):
        """Flag whether any draft journal entries exist in the locked period."""
        AccountMove = self.env['account.move']
        for wiz in self:
            has_drafts = bool(AccountMove.search(
                wiz._build_draft_moves_domain(), limit=1,
            ))
            wiz.show_draft_entries_warning = has_drafts

    def _get_changes_needing_exception(self):
        """Identify soft lock fields that are being loosened
        (i.e. the new date is earlier than the current company setting)."""
        self.ensure_one()
        company = self.env.company
        relaxed = {}
        for fld in SOFT_LOCK_DATE_FIELDS:
            current_val = company[fld]
            new_val = self[fld]
            if current_val and (not new_val or new_val < current_val):
                relaxed[fld] = new_val
        return relaxed

    @api.depends(*SOFT_LOCK_DATE_FIELDS)
    def _compute_exception_needed(self):
        for wiz in self:
            wiz.exception_needed = bool(wiz._get_changes_needing_exception())

    @api.depends(*SOFT_LOCK_DATE_FIELDS)
    def _compute_exception_needed_fields(self):
        for wiz in self:
            relaxed_fields = wiz._get_changes_needing_exception()
            wiz.exception_needed_fields = ','.join(relaxed_fields)

    # -------------------------------------------------------------------------
    # Lock Date Value Preparation
    # -------------------------------------------------------------------------

    def _prepare_lock_date_values(self, exception_vals_list=None):
        """Return a dict of lock-date fields that have changed and
        are not covered by a newly created exception."""
        self.ensure_one()
        company = self.env.company

        # Hard lock date can never decrease
        if company.hard_lock_date and (
            not self.hard_lock_date or self.hard_lock_date < company.hard_lock_date
        ):
            raise UserError(_(
                'The Hard Lock Date cannot be decreased or removed once set.'
            ))

        changed_vals = {}
        for fld in LOCK_DATE_FIELDS:
            if self[fld] != company[fld]:
                changed_vals[fld] = self[fld]

        # No lock date may be set in the future
        today = fields.Date.context_today(self)
        for fld, val in changed_vals.items():
            if val and val > today:
                raise UserError(_(
                    'A Lock Date cannot be set to a future date.'
                ))

        # Exclude fields that are being handled via exception creation
        if exception_vals_list:
            for exc_vals in exception_vals_list:
                for fld in LOCK_DATE_FIELDS:
                    if fld in exc_vals:
                        changed_vals.pop(fld, None)

        return changed_vals

    def _prepare_exception_values(self):
        """Construct a list of dicts suitable for creating
        account.lock_exception records for any loosened lock dates."""
        self.ensure_one()
        relaxed = self._get_changes_needing_exception()
        if not relaxed:
            return False

        # Relaxing for everyone forever is equivalent to simply updating the date
        if self.exception_applies_to == 'everyone' and self.exception_duration == 'forever':
            return False

        # Validate that scope and duration are set
        validation_issues = []
        if not self.exception_applies_to:
            validation_issues.append(_('Please select who the exception applies to.'))
        if not self.exception_duration:
            validation_issues.append(_('Please select a duration for the exception.'))
        if validation_issues:
            raise UserError('\n'.join(validation_issues))

        # Build shared exception values
        shared_vals = {
            'company_id': self.env.company.id,
        }

        # Determine target user
        scope_to_user = {
            'me': self.env.user.id,
            'everyone': False,
        }
        shared_vals['user_id'] = scope_to_user[self.exception_applies_to]

        # Determine expiration
        duration_map = {
            '5min': timedelta(minutes=5),
            '15min': timedelta(minutes=15),
            '1h': timedelta(hours=1),
            '24h': timedelta(hours=24),
            'forever': False,
        }
        delta = duration_map[self.exception_duration]
        if delta:
            shared_vals['end_datetime'] = self.env.cr.now() + delta

        if self.exception_reason:
            shared_vals['reason'] = self.exception_reason

        return [
            {**shared_vals, fld: val}
            for fld, val in relaxed.items()
        ]

    # -------------------------------------------------------------------------
    # Period Date Helpers
    # -------------------------------------------------------------------------

    def _get_current_period_dates(self, lock_date_field):
        """Determine the start and end of the current period relative
        to the selected lock date (using the prior lock date or fiscal year start)."""
        self.ensure_one()
        existing_lock = self.env.company[lock_date_field]
        if existing_lock:
            period_start = existing_lock + timedelta(days=1)
        else:
            period_start = date_utils.get_fiscal_year(self[lock_date_field])[0]
        return period_start, self[lock_date_field]

    def _create_default_report_external_values(self, lock_date_field):
        """Hook for generating default report external values when lock
        dates change. Extended by account reporting modules."""
        date_from, date_to = self._get_current_period_dates(lock_date_field)
        self.env['account.report']._generate_default_external_values(
            date_from, date_to, lock_date_field == 'tax_lock_date',
        )

    # -------------------------------------------------------------------------
    # Core Lock Date Application
    # -------------------------------------------------------------------------

    def _change_lock_date(self, lock_date_values=None):
        """Apply the given lock date values to the company, generating
        default report external values as needed."""
        self.ensure_one()
        if lock_date_values is None:
            lock_date_values = self._prepare_lock_date_values()

        # Handle tax lock date report externals
        new_tax_lock = lock_date_values.get('tax_lock_date')
        if new_tax_lock and new_tax_lock != self.env.company['tax_lock_date']:
            self._create_default_report_external_values('tax_lock_date')

        # Handle fiscal year / hard lock date report externals
        new_fy_lock = lock_date_values.get('fiscalyear_lock_date')
        new_hard_lock = lock_date_values.get('hard_lock_date')
        if new_fy_lock or new_hard_lock:
            candidate_lock, candidate_field = max(
                [(new_fy_lock, 'fiscalyear_lock_date'),
                 (new_hard_lock, 'hard_lock_date')],
                key=lambda pair: pair[0] or date.min,
            )
            existing_fy_max = max(
                self.env.company.fiscalyear_lock_date or date.min,
                self.env.company.hard_lock_date or date.min,
            )
            if candidate_lock != existing_fy_max:
                self._create_default_report_external_values(candidate_field)

        self.env.company.sudo().write(lock_date_values)

    def change_lock_date(self):
        """Main action: validate permissions, create exceptions if needed,
        and apply new lock dates to the company."""
        self.ensure_one()
        if not self.env.user.has_group('account.group_account_manager'):
            raise UserError(_(
                'Only Billing Administrators are allowed to change lock dates!'
            ))

        exc_vals_list = self._prepare_exception_values()
        updated_lock_vals = self._prepare_lock_date_values(
            exception_vals_list=exc_vals_list,
        )

        if exc_vals_list:
            self.env['account.lock_exception'].create(exc_vals_list)

        self._change_lock_date(updated_lock_vals)
        return {'type': 'ir.actions.act_window_close'}

    # -------------------------------------------------------------------------
    # UI Actions
    # -------------------------------------------------------------------------

    def action_show_draft_moves_in_locked_period(self):
        """Open a list view showing draft moves within the locked period."""
        self.ensure_one()
        return {
            'view_mode': 'list',
            'name': _('Draft Entries'),
            'res_model': 'account.move',
            'type': 'ir.actions.act_window',
            'domain': self._build_draft_moves_domain(),
            'search_view_id': [
                self.env.ref('account.view_account_move_filter').id, 'search',
            ],
            'views': [
                [self.env.ref('account.view_move_tree_multi_edit').id, 'list'],
                [self.env.ref('account.view_move_form').id, 'form'],
            ],
        }

    def action_reopen_wizard(self):
        """Return an action that reopens this wizard in a dialog."""
        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    def _action_revoke_min_exception(self, exception_field):
        """Revoke a specific exception and refresh the wizard."""
        self.ensure_one()
        target_exception = self[exception_field]
        if target_exception:
            target_exception.action_revoke()
            self._compute_lock_date_exceptions()
        return self.action_reopen_wizard()

    # --- Per-field revoke actions (current user) ---
    def action_revoke_min_sale_lock_date_exception_for_me(self):
        return self._action_revoke_min_exception('min_sale_lock_date_exception_for_me_id')

    def action_revoke_min_purchase_lock_date_exception_for_me(self):
        return self._action_revoke_min_exception('min_purchase_lock_date_exception_for_me_id')

    def action_revoke_min_tax_lock_date_exception_for_me(self):
        return self._action_revoke_min_exception('min_tax_lock_date_exception_for_me_id')

    def action_revoke_min_fiscalyear_lock_date_exception_for_me(self):
        return self._action_revoke_min_exception('min_fiscalyear_lock_date_exception_for_me_id')

    # --- Per-field revoke actions (everyone) ---
    def action_revoke_min_sale_lock_date_exception_for_everyone(self):
        return self._action_revoke_min_exception('min_sale_lock_date_exception_for_everyone_id')

    def action_revoke_min_purchase_lock_date_exception_for_everyone(self):
        return self._action_revoke_min_exception('min_purchase_lock_date_exception_for_everyone_id')

    def action_revoke_min_tax_lock_date_exception_for_everyone(self):
        return self._action_revoke_min_exception('min_tax_lock_date_exception_for_everyone_id')

    def action_revoke_min_fiscalyear_lock_date_exception_for_everyone(self):
        return self._action_revoke_min_exception('min_fiscalyear_lock_date_exception_for_everyone_id')
