from datetime import datetime, time

from dateutil.relativedelta import relativedelta
from pytz import timezone, utc

from odoo import _, api, fields, models


class ResourceCalendarLeaves(models.Model):
    _name = "resource.calendar.leaves"
    _inherit = ["resource.calendar.leaves"]

    xeno_holiday_type = fields.Selection(
        [("public", "Public"), ("company", "Company")],
        string="Type", default="public", index=True,
        help="Mirrors XENHR's Holiday.type field (public/company).",
    )
    xeno_remark = fields.Text(
        string="Description",
        help="Mirrors XENHR's Holiday.description field "
             "(Thai annotation for public holidays).",
    )
    xeno_date = fields.Date(
        string="Date",
        compute="_compute_xeno_date", inverse="_inverse_xeno_date",
        help="Single-day shortcut used by the Add Holiday quick-create "
             "dialog: writing it spans date_from/date_to over that whole "
             "day, matching the storage convention of the imported "
             "holiday rows (00:00:00 - 23:59:59).",
    )
    xeno_recurring_yearly = fields.Boolean(
        string="Recurring yearly (same day each year)",
        help="Mirrors XENHR's recurring-holiday flag. Next year's copy is "
             "created immediately on save, and a scheduled action keeps "
             "rolling each further year forward.",
    )

    @api.depends("date_from")
    def _compute_xeno_date(self):
        for leave in self:
            leave.xeno_date = leave.date_from.date() if leave.date_from else False

    def _inverse_xeno_date(self):
        # date_from/date_to are stored in UTC, but xeno_date is a plain
        # calendar day with no timezone of its own -- naively combining it
        # with time.min/time.max and writing that straight to date_from
        # (as an earlier version of this did) silently treats "midnight
        # local" as "midnight UTC". For a positive-offset timezone (e.g.
        # Asia/Bangkok, UTC+7) that pushes date_to's 23:59:59 past midnight
        # UTC and into the next calendar day once the calendar view
        # converts it back to local time for display -- a single-day
        # holiday visually spans two day cells. Localize to the same
        # timezone core's own _compute_date_to resolves (context/user tz,
        # else the calendar's/company's) before converting to UTC, so both
        # ends land back on the one intended local day.
        for leave in self:
            if leave.xeno_date:
                tz_name = self.env.context.get("tz") or self.env.user.tz \
                    or leave.calendar_id.tz or self.env.company.resource_calendar_id.tz or "UTC"
                tz = timezone(tz_name)
                local_from = tz.localize(datetime.combine(leave.xeno_date, time.min))
                leave.date_from = local_from.astimezone(utc).replace(tzinfo=None)
                # date_to is core's own compute="_compute_date_to", store=True,
                # readonly=False field, already deriving "23:59:59 in that
                # same tz" from date_from -- let it, instead of duplicating
                # (and previously miscomputing) that logic here.

    @api.model_create_multi
    def create(self, vals_list):
        leaves = super().create(vals_list)
        # Skip when materializing next-year copies ourselves, or the
        # copy's own create() would recurse another year forward.
        if not self.env.context.get("xeno_skip_recur"):
            leaves._xeno_ensure_next_year_copy()
            leaves._xeno_auto_deduct_company_holiday()
        return leaves

    def _xeno_local_day(self, dt):
        """Convert a UTC-stored date_from/date_to datetime to the plain
        local calendar day it actually represents -- same tz resolution
        _inverse_xeno_date uses, just the reverse direction."""
        tz_name = self.env.context.get("tz") or self.env.user.tz \
            or self.calendar_id.tz or self.env.company.resource_calendar_id.tz or "UTC"
        return utc.localize(dt).astimezone(timezone(tz_name)).date()

    def _xeno_auto_deduct_company_holiday(self):
        """Company holiday policy (per susu, not Odoo's stock assumption):
        a declared Company Holiday isn't automatically paid time off -- HR
        deducts a real leave day for every employee who already has leave
        allocated, cascading Annual -> Personal -> Unpaid (see hr.leave's
        _xeno_deduct_for_company_holiday, shared with the manual
        wizard/leave_holiday_deduct.py). Only for genuinely new, company-
        wide (no resource_id) "company"-type holidays -- a "public" holiday
        keeps Odoo's normal paid-non-working-day behavior, and auto-
        generated next-year recurring copies are skipped by the
        xeno_skip_recur guard in create() above (deducting against a year
        that doesn't have allocations yet would just fall through to
        Unpaid Leave for everyone, which isn't useful).
        """
        Leave = self.env["hr.leave"].sudo()
        Employee = self.env["hr.employee"].sudo()
        Alloc = self.env["hr.leave.allocation"].sudo()
        cascade_type_ids = Leave._xeno_holiday_deduct_cascade_types().ids
        for leave in self:
            if leave.xeno_holiday_type != "company" or leave.resource_id or not leave.date_from:
                continue
            date_from = self._xeno_local_day(leave.date_from)
            date_to = self._xeno_local_day(leave.date_to) if leave.date_to else date_from
            employees = Employee.search([
                ("id", "in", Alloc.search([
                    ("holiday_status_id", "in", cascade_type_ids),
                    ("state", "=", "validate"),
                ]).mapped("employee_id").ids),
            ])
            Leave._xeno_deduct_for_company_holiday(
                employees, date_from, date_to,
                reason=_("Company holiday deduction: %s", leave.name),
                source_holiday=leave,
            )

    def unlink(self):
        # Give the day back: deleting a Company Holiday should reverse
        # whatever it auto-deducted, not leave employees permanently
        # short a day for a holiday that no longer exists. Deliberately
        # unscoped by xeno_holiday_type here -- xeno_source_holiday_id is
        # only ever set by the company-holiday hook above, so the search
        # naturally only ever matches those.
        Leave = self.env["hr.leave"].sudo()
        leaves = Leave.search([("xeno_source_holiday_id", "in", self.ids)])
        leaves._xeno_reverse_holiday_deduction()
        return super().unlink()

    def _xeno_ensure_next_year_copy(self):
        """For each recurring holiday, make sure the same holiday exists
        one year later (same day/month). Idempotent: matched by name +
        company + calendar within the target day."""
        for leave in self.filtered(
                lambda l: l.xeno_recurring_yearly and l.date_from
                and not l.resource_id):
            nxt_from = leave.date_from + relativedelta(years=1)
            nxt_to = (leave.date_to or leave.date_from) + relativedelta(years=1)
            exists = self.with_context(active_test=False).search_count([
                ("name", "=", leave.name),
                ("company_id", "=", leave.company_id.id),
                ("calendar_id", "=", leave.calendar_id.id),
                ("resource_id", "=", False),
                ("date_from", ">=", datetime.combine(nxt_from.date(), time.min)),
                ("date_from", "<=", datetime.combine(nxt_from.date(), time.max)),
            ])
            if not exists:
                leave.with_context(xeno_skip_recur=True).copy({
                    "date_from": nxt_from,
                    "date_to": nxt_to,
                })

    @api.model
    def _cron_xeno_recur_holidays(self):
        """Monthly, idempotent: every recurring holiday dated within the
        last year gets its next-year copy ensured, so the chain keeps
        rolling one year ahead even if a copy was deleted or the server
        was down when a create-time copy should have happened."""
        cutoff = fields.Datetime.now() - relativedelta(years=1)
        self.search([
            ("xeno_recurring_yearly", "=", True),
            ("resource_id", "=", False),
            ("date_from", ">=", cutoff),
        ])._xeno_ensure_next_year_copy()
