# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import date
from odoo import api, fields, models
from odoo.fields import Domain
from odoo.exceptions import ValidationError


class HrVersion(models.Model):
    """ Write and Create:
        Special case when setting a contract as running:
        If there is already a validated time off over another contract
        with a different schedule, split the time off, before the
        _check_contracts raises an issue.
        If there are existing leaves that are spanned by this new
        contract, update their resource calendar to the current one.
    """
    # TODO BIOUTIFY ME (the whole file :)
    _inherit = 'hr.version'
    _description = 'Employee Contract'

    @api.constrains('contract_date_start', 'contract_date_end')
    def _check_contracts(self):
        self._get_leaves()._check_contracts()

    @api.model_create_multi
    def create(self, vals_list):
        all_new_leave_origin = []
        all_new_leave_vals = []
        leaves_state = {}
        created_versions = self.env['hr.version']
        for vals in vals_list:
            if not 'employee_id' in vals or not 'resource_calendar_id' in vals:
                created_versions |= super().create(vals)
                continue
            leaves = self._get_leaves_from_vals(vals)
            is_created = False
            for leave in leaves:
                leaves_state = self._refuse_leave(leave, leaves_state) if leave.request_date_from < vals['contract_date_start'] else self._set_leave_draft(leave, leaves_state)
                if not is_created:
                    created_versions |= super().create([vals])
                    is_created = True
                overlapping_contracts = self._check_overlapping_contract(leave)
                if not overlapping_contracts:
                    # When the leave is set to draft
                    leave._compute_date_from_to()
                    continue
                all_new_leave_origin, all_new_leave_vals = self._populate_all_new_leave_vals_from_split_leave(
                    all_new_leave_origin, all_new_leave_vals, overlapping_contracts, leave, leaves_state)
            # TODO FIXME
            # to keep creation order, not ideal but ok for now.
            if not is_created:
                created_versions |= super().create([vals])
        try:
            if all_new_leave_vals:
                self._create_all_new_leave(all_new_leave_origin, all_new_leave_vals)
        except ValidationError:
            # In case a validation error is thrown due to holiday creation with the new resource calendar (which can
            # increase their duration), we catch this error to display a more meaningful error message.
            raise ValidationError(
                self.env._("Changing the contract on this employee changes their working schedule in a period "
                           "they already took leaves. Changing this working schedule changes the duration of "
                           "these leaves in such a way the employee no longer has the required allocation for "
                           "them. Please review these leaves and/or allocations before changing the contract."))
        return created_versions

    def write(self, vals):
        specific_contracts = self.env['hr.version']
        if any(field in vals for field in ['contract_date_start', 'contract_date_end', 'date_version', 'resource_calendar_id']):
            all_new_leave_origin = []
            all_new_leave_vals = []
            leaves_state = {}
            try:
                for contract in self:
                    resource_calendar_id = vals.get('resource_calendar_id', contract.resource_calendar_id.id)
                    extra_domain = [('resource_calendar_id', '!=', resource_calendar_id)] if resource_calendar_id else None
                    leaves = contract._get_leaves(
                        extra_domain=extra_domain
                    )
                    for leave in leaves:
                        super(HrVersion, contract).write(vals)
                        overlapping_contracts = self._check_overlapping_contract(leave)
                        if not overlapping_contracts:
                            continue
                        leaves_state = self._refuse_leave(leave, leaves_state)
                        specific_contracts += contract
                        all_new_leave_origin, all_new_leave_vals = self._populate_all_new_leave_vals_from_split_leave(
                            all_new_leave_origin, all_new_leave_vals, overlapping_contracts, leave, leaves_state)
                if all_new_leave_vals:
                    self._create_all_new_leave(all_new_leave_origin, all_new_leave_vals)
            except ValidationError:
                # In case a validation error is thrown due to holiday creation with the new resource calendar (which can
                # increase their duration), we catch this error to display a more meaningful error message.
                raise ValidationError(self.env._("Changing the contract on this employee changes their working schedule in a period "
                                        "they already took leaves. Changing this working schedule changes the duration of "
                                        "these leaves in such a way the employee no longer has the required allocation for "
                                        "them. Please review these leaves and/or allocations before changing the contract."))
        return super(HrVersion, self - specific_contracts).write(vals)

    def _get_leaves(self, extra_domain=None):
        domain = [
            ('state', '!=', 'refuse'),
            ('employee_id', 'in', self.mapped('employee_id.id')),
            ('date_from', '<=', max(end or date.max for end in self.sudo().mapped('contract_date_end'))),
            ('date_to', '>=', min(self.sudo().mapped('contract_date_start'))),
        ]
        if extra_domain:
            domain = Domain.AND([domain, extra_domain])
        return self.env['hr.leave'].search(domain)

    def _get_leaves_from_vals(self, vals):
        domain = [
            ('state', '!=', 'refuse'),
            ('employee_id', 'in', vals['employee_id']),
            ('date_to', '>=', fields.Date.from_string(vals.get('contract_date_start', vals.get('date_version', fields.Date.today())))),
            ('resource_calendar_id', '!=', vals.get('resource_calendar_id')),
        ]
        if vals.get('contract_date_end'):
            domain = Domain.AND([domain, [('date_from', '<=', fields.Date.from_string(vals['contract_date_end']))]])
        return self.env['hr.leave'].search(domain)

    def _check_overlapping_contract(self, leave):
        # Get all overlapping contracts but exclude draft contracts that are not included in this transaction.
        overlapping_contracts = leave._get_overlapping_contracts().sorted(
            key=lambda c: c.contract_date_start)
        if len(overlapping_contracts.resource_calendar_id) <= 1:
            if overlapping_contracts:
                first_overlapping_contract = next(iter(overlapping_contracts), overlapping_contracts)
                if leave.resource_calendar_id != first_overlapping_contract.resource_calendar_id:
                    leave.resource_calendar_id = first_overlapping_contract.resource_calendar_id
                    if not leave.request_unit_hours:
                        leave.with_context(leave_skip_date_check=True, leave_skip_state_check=True)._compute_date_from_to()
                        if leave.state == 'validate':
                            leave._validate_leave_request()
            return False
        return overlapping_contracts

    def _refuse_leave(self, leave, leaves_state):
        if leave.id not in leaves_state:
            leaves_state[leave.id] = leave.state
        if leave.state not in ['refuse', 'confirm']:
            leave.action_refuse()
        return leaves_state

    def _set_leave_draft(self, leave, leaves_state):
        if leave.id not in leaves_state:
            leaves_state[leave.id] = leave.state
        if leave.state not in ['refuse', 'confirm']:
            leave.action_back_to_approval()
        return leaves_state

    def _populate_all_new_leave_vals_from_split_leave(self, all_new_leave_origin, all_new_leave_vals, overlapping_contracts, leave, leaves_state):
        last_version = overlapping_contracts[-1]
        for overlapping_contract in overlapping_contracts:
            new_request_date_from = max(leave.request_date_from, overlapping_contract.contract_date_start)
            new_request_date_to = min(leave.request_date_to, overlapping_contract.contract_date_end or date.max)
            new_leave_vals = leave.copy_data({
                'request_date_from': new_request_date_from,
                'request_date_to': new_request_date_to,
                'state': leaves_state[leave.id] if overlapping_contract.id != last_version.id else 'confirm',
            })[0]
            new_leave = self.env['hr.leave'].new(new_leave_vals)
            new_leave._compute_date_from_to()
            new_leave._compute_duration()
            # Could happen for part-time contract, that time off is not necessary
            # anymore.
            if new_leave.date_from < new_leave.date_to:
                all_new_leave_origin.append(leave)
                all_new_leave_vals.append(new_leave._convert_to_write(new_leave._cache))
        return all_new_leave_origin, all_new_leave_vals

    def _create_all_new_leave(self, all_new_leave_origin, all_new_leave_vals):
        new_leaves = self.env['hr.leave'].with_context(
            tracking_disable=True,
            mail_activity_automation_skip=True,
            leave_fast_create=True,
            leave_skip_state_check=True
        ).create(all_new_leave_vals)
        new_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request()
        for index, new_leave in enumerate(new_leaves):
            new_leave.message_post_with_source(
                'mail.message_origin_link',
                render_values={'self': new_leave, 'origin': all_new_leave_origin[index]},
                subtype_xmlid='mail.mt_note',
            )
