from datetime import timedelta

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError

STATES = [
    ("draft", "Draft"),
    ("submitted", "Submitted"),
    ("approved", "Approved"),
    ("rejected", "Rejected"),
    ("charged", "Charged / Completed"),
]


class HrOvertimeTimesheet(models.Model):
    """Weekly OT timesheet: groups every HOD-approved hr.overtime.request
    that falls in one calendar week (Monday-Sunday) for one employee, so
    the employee enters actual In/Out/Break per day and submits the WHOLE
    WEEK at once for a single Head of Department approval -- matching the
    real reference system's weekly timesheet screen exactly (one shared
    "Head of Department Authorisation" per week, not one per OT day).
    """

    _name = "hr.overtime.timesheet"
    _description = "Weekly Overtime Timesheet"
    _inherit = ["mail.thread"]
    _order = "week_start desc, id desc"

    name = fields.Char(compute="_compute_name", store=True)
    employee_id = fields.Many2one(
        "hr.employee", required=True, tracking=True,
        default=lambda self: self.env.user.employee_id.id)
    department_id = fields.Many2one(related="employee_id.department_id", string="Department")
    week_start = fields.Date(
        required=True, tracking=True, default=fields.Date.context_today,
        help="Snapped to the Monday of whichever week you pick.")
    week_end = fields.Date(compute="_compute_week_end", store=True)

    request_ids = fields.One2many("hr.overtime.request", "timesheet_id", string="Days")
    # store=True is required here, not just a nice-to-have: the record
    # rule (own-employee OR configured Supervisor/HOD) filters on these
    # fields, and Odoo can't compile a non-stored field into the SQL a
    # record rule needs -- confirmed live ("Cannot convert ... to SQL
    # because it is not stored") the moment the HOD tried to browse the
    # record.
    supervisor_id = fields.Many2one(
        "hr.employee", compute="_compute_approvers", store=True, string="Supervisor (Manager)")
    head_of_department_id = fields.Many2one(
        "hr.employee", compute="_compute_approvers", store=True, string="Head of Department")

    state = fields.Selection(STATES, default="draft", required=True, tracking=True, copy=False)
    rejection_reason = fields.Char(copy=False)
    submitted_at = fields.Datetime(readonly=True, copy=False)
    hod_decided_by = fields.Many2one("res.users", readonly=True, copy=False)
    hod_decided_at = fields.Datetime(readonly=True, copy=False)
    charged_at = fields.Datetime(readonly=True, copy=False)

    total_hrs_work = fields.Float(string="Weekly Hrs Work", compute="_compute_totals", store=True)
    total_ot_hrs = fields.Float(string="Weekly OT Hrs", compute="_compute_totals", store=True)

    can_submit = fields.Boolean(compute="_compute_can_flags")
    can_hod_decide = fields.Boolean(compute="_compute_can_flags")
    can_hr_decide = fields.Boolean(compute="_compute_can_flags")

    @api.depends("employee_id", "week_start", "week_end")
    def _compute_name(self):
        for rec in self:
            if rec.week_start and rec.week_end:
                rec.name = _(
                    "%(employee)s - Overtime Timesheet %(start)s to %(end)s",
                    employee=rec.employee_id.name or "", start=rec.week_start, end=rec.week_end)
            else:
                rec.name = rec.employee_id.name or _("New Overtime Timesheet")

    @api.depends("week_start")
    def _compute_week_end(self):
        for rec in self:
            rec.week_end = rec.week_start + timedelta(days=6) if rec.week_start else False

    @api.depends("request_ids.supervisor_id", "request_ids.head_of_department_id")
    def _compute_approvers(self):
        # Routing is per-request (the employee names both on each OT
        # request), so in the practical case of one employee always
        # naming the same Supervisor/HOD, the first linked day's choice
        # stands for the whole week -- documented simplification, not a
        # per-line check.
        for rec in self:
            first = rec.request_ids[:1]
            rec.supervisor_id = first.supervisor_id
            rec.head_of_department_id = first.head_of_department_id

    @api.depends("request_ids.hrs_worked", "request_ids.computed_ot_hours")
    def _compute_totals(self):
        for rec in self:
            rec.total_hrs_work = sum(rec.request_ids.mapped("hrs_worked"))
            rec.total_ot_hrs = sum(rec.request_ids.mapped("computed_ot_hours"))

    @api.depends_context("uid")
    @api.depends("employee_id", "state", "head_of_department_id")
    def _compute_can_flags(self):
        is_hr = self._xeno_is_hr()
        for rec in self:
            is_owner = self.env.user.employee_id == rec.employee_id
            rec.can_submit = (is_hr or is_owner) and rec.state in ("draft", "rejected")
            rec.can_hod_decide = (is_hr or rec._xeno_is_hod()) and rec.state == "submitted"
            rec.can_hr_decide = is_hr and rec.state == "approved"

    def _xeno_is_hod(self):
        self.ensure_one()
        return bool(self.head_of_department_id) and self.env.user.employee_id == self.head_of_department_id

    def _xeno_is_hr(self):
        return self.env.is_superuser() or self.env.user.has_group("hr_holidays.group_hr_holidays_user")

    @api.onchange("week_start")
    def _onchange_week_start_snap_monday(self):
        if self.week_start:
            self.week_start = self.week_start - timedelta(days=self.week_start.weekday())

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get("week_start"):
                d = fields.Date.to_date(vals["week_start"])
                vals["week_start"] = d - timedelta(days=d.weekday())
        records = super().create(vals_list)
        records.action_pull_week_requests()
        return records

    _XENO_TRANSITION_FIELDS = {
        "state", "hod_decided_by", "hod_decided_at", "rejection_reason",
        "submitted_at", "charged_at",
    }

    def write(self, vals):
        if not self.env.su and not self._xeno_is_hr():
            touched = set(vals)
            if touched & self._XENO_TRANSITION_FIELDS:
                raise UserError(_(
                    "Use Submit/Approve/Reject actions to change a timesheet's "
                    "status -- it can't be edited directly."))
            for rec in self:
                if {"employee_id", "week_start"} & touched:
                    if rec.state not in ("draft", "rejected"):
                        raise UserError(_(
                            "This timesheet can no longer be edited once submitted. "
                            "Ask HR/Payroll to update it."))
                    if self.env.user.employee_id != rec.employee_id:
                        raise UserError(_("Only the requesting employee can edit this timesheet."))
        return super().write(vals)

    def action_pull_week_requests(self):
        """Links every one of this employee's HOD-approved OT requests
        that falls in this week and isn't already on another timesheet.
        Safe to re-run (e.g. an OT request gets HOD-approved after this
        timesheet was created but before it's submitted) -- callable as
        long as the timesheet itself is still Draft/Rejected."""
        for rec in self:
            if rec.state not in ("draft", "rejected"):
                continue
            pending = self.env["hr.overtime.request"].sudo().search([
                ("employee_id", "=", rec.employee_id.id),
                ("state", "=", "hod_approved"),
                ("timesheet_id", "=", False),
                ("date", ">=", rec.week_start),
                ("date", "<=", rec.week_end),
            ])
            if pending:
                pending.write({"timesheet_id": rec.id})

    def action_submit(self):
        for rec in self:
            if rec.state not in ("draft", "rejected"):
                raise UserError(_("Only a Draft or Rejected timesheet can be submitted."))
            if not rec.can_submit:
                raise UserError(_("Only the employee themselves can submit their timesheet."))
            if not rec.request_ids:
                raise UserError(_(
                    "No HOD-approved OT requests found for this week -- nothing to submit."))
            missing = rec.request_ids.filtered(lambda r: not r.time_in or not r.time_out)
            if missing:
                raise UserError(_(
                    "Enter In Time and Out Time for every day before submitting "
                    "(missing: %s).", ", ".join(missing.mapped(lambda r: str(r.date)))))
            rec.sudo().write({
                "state": "submitted", "submitted_at": fields.Datetime.now(),
                "rejection_reason": False,
            })
            rec.message_post(body=_("Timesheet submitted for Head of Department approval."))
            rec._xeno_notify_hod(_(
                "%(employee)s submitted their Overtime Timesheet for %(start)s to %(end)s "
                "(%(hours).2f OT hour(s)) -- awaiting your approval.",
                employee=rec.employee_id.name, start=rec.week_start, end=rec.week_end,
                hours=rec.total_ot_hrs))

    def action_hod_approve(self):
        for rec in self:
            if rec.state != "submitted":
                raise UserError(_("Only a Submitted timesheet can be approved."))
            if not (rec._xeno_is_hod() or rec._xeno_is_hr()):
                raise UserError(_("Only the configured Head of Department can approve this timesheet."))
            rec.sudo().write({
                "state": "approved",
                "hod_decided_by": self.env.user.id,
                "hod_decided_at": fields.Datetime.now(),
            })
            rec.message_post(body=_("Approved by Head of Department: %s", self.env.user.name))
            rec._xeno_notify_employee(_(
                "Your Overtime Timesheet for %(start)s to %(end)s was approved by "
                "your Head of Department.", start=rec.week_start, end=rec.week_end))

    def action_hod_reject(self, reason=False):
        for rec in self:
            if rec.state != "submitted":
                raise UserError(_("Only a Submitted timesheet can be rejected."))
            if not (rec._xeno_is_hod() or rec._xeno_is_hr()):
                raise UserError(_("Only the configured Head of Department can reject this timesheet."))
            rec.sudo().write({"state": "rejected", "rejection_reason": reason or False})
            rec.message_post(body=_("Rejected by Head of Department: %s", self.env.user.name))
            rec._xeno_notify_employee(_(
                "Your Overtime Timesheet for %(start)s to %(end)s was rejected. "
                "Please review and resubmit.", start=rec.week_start, end=rec.week_end))

    def action_hr_calculate_charges(self):
        for rec in self:
            if rec.state != "approved":
                raise UserError(_("Only an HOD-approved timesheet can be charged."))
            if not rec._xeno_is_hr():
                raise UserError(_("Only HR/Payroll can calculate OT charges."))
            if rec.total_ot_hrs <= 0:
                raise UserError(_(
                    "Weekly OT Hrs must be greater than 0 -- check the In/Out "
                    "Time and Break Hours on each day before charging."))
            rec.sudo().write({"state": "charged", "charged_at": fields.Datetime.now()})
            rec.message_post(body=_(
                "OT charged: %.2f hour(s) for the week, by %s", rec.total_ot_hrs, self.env.user.name))
            rec._xeno_notify_employee(_(
                "Your Overtime Timesheet for %(start)s to %(end)s has been finalized: "
                "%(hours).2f hour(s) charged.", start=rec.week_start, end=rec.week_end,
                hours=rec.total_ot_hrs))

    def unlink(self):
        if not self.env.su and not self._xeno_is_hr():
            for rec in self:
                if rec.state not in ("draft", "rejected") or self.env.user.employee_id != rec.employee_id:
                    raise UserError(_(
                        "Only your own Draft/Rejected timesheet can be deleted; "
                        "once submitted, ask HR/Payroll."))
        for rec in self:
            rec.request_ids.sudo().write({"timesheet_id": False})
        return super().unlink()

    def _xeno_notify_employee(self, message):
        self.ensure_one()
        self.message_post(body=message)
        email = self.employee_id.work_email or (self.employee_id.user_id.email if self.employee_id.user_id else False)
        if email:
            self.env["mail.mail"].sudo().create({
                "subject": _("Overtime Timesheet Update"),
                "body_html": "<p>%s</p>" % message,
                "email_to": email, "auto_delete": True,
            }).send()

    def _xeno_notify_hod(self, message):
        self.ensure_one()
        hod = self.head_of_department_id
        email = hod.work_email or (hod.user_id.email if hod.user_id else False)
        if email:
            self.env["mail.mail"].sudo().create({
                "subject": _("Overtime Timesheet Awaiting Approval"),
                "body_html": "<p>%s</p>" % message,
                "email_to": email, "auto_delete": True,
            }).send()
