import time
from datetime import datetime, timedelta
from datetime import time as dt_time

from dateutil.relativedelta import relativedelta

from odoo import _, api, models
from odoo.tools import html_escape
from odoo.tools.misc import hmac as odoo_hmac

_ANALYSIS_ACTIVE_STATES = ("confirm", "validate1", "validate")

# Mirrors xeno_leave/static/src/js/leave_type_colors.js's
# LEAVE_TYPE_FONT_COLORS exactly -- kept in sync by hand, same reasoning
# as _xeno_render_analysis_html below (an email can't import browser JS).
_LEAVE_TYPE_FONT_COLORS = {
    "AL": "#7659ff",
    "PL": "#ff9e2e",
    "SL": "#fb919e",
    "ML": "#fd6dbf",
    "UL": "#9f9f9f",
    "BT": "#0e7490",
}
_DEFAULT_LEAVE_TYPE_FONT_COLOR = "#9f9f9f"


def _leave_type_font_color(display_name):
    if not display_name:
        return _DEFAULT_LEAVE_TYPE_FONT_COLOR
    idx = display_name.rfind(".")
    abbr = display_name.strip() if idx == -1 else display_name[idx + 1:].strip()
    return _LEAVE_TYPE_FONT_COLORS.get(abbr, _DEFAULT_LEAVE_TYPE_FONT_COLOR)

TOKEN_SCOPE = "xeno_leave_email_action"
TOKEN_VALID_SECONDS = 14 * 24 * 3600  # 14 days -- a leave request going stale
# for that long should be handled in-app by then, but this bounds the
# lifetime of a link that might sit unread in an inbox.

_BASE_TYPE_LABELS = {
    "full_day": "Full-day Leave",
    "part_time": "Part-time Leave",
    "first_half": "First-half Day Leave",
    "second_half": "Second-half Day Leave",
}

_HEADER = """
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;color:#142933;border:1px solid #d8e1e4;">
  <div style="background:#142933;padding:14px 20px;">
    <div style="color:#fff;">
      <div style="font-weight:700;font-size:16px;">XenOptics HR System</div>
      <div style="font-size:12px;color:#b7c7cc;">Employee Leave Request</div>
    </div>
  </div>
  <div style="padding:20px;">
    <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;">
      <h2 style="margin:0;font-size:20px;">Employee Leave Request</h2>
      <a href="%(see_more)s" style="background:#ec1d23;color:#fff;padding:5px 12px;border-radius:14px;font-size:11px;text-decoration:none;">See More</a>
    </div>
    <div style="font-size:13px;margin-bottom:10px;"><strong>Employee Leave Request (%(status)s)</strong></div>
    %(rows_before)s
    %(table)s
    %(rows_after)s
    %(history)s
    %(analysis)s
"""

_FOOTER = """
  </div>
  <div style="background:#142933;color:#b7c7cc;text-align:center;padding:8px;font-size:11px;">
    XenOptics HR &middot; Do not reply
  </div>
</div>
"""


def _esc(s):
    return html_escape(s if s is not None else "")


def _row_pair(label, value):
    return '<div style="margin:6px 0;font-size:13px;"><strong>%s:</strong> %s</div>' % (label, value)


def _format_decided_at(decided_at):
    """decided_at is stored as naive UTC (Odoo convention) -- shift to
    Asia/Bangkok (UTC+7) for display, matching susu's own template."""
    if not decided_at:
        return ""
    local = decided_at + timedelta(hours=7)
    return local.strftime("%b %d, %Y %I:%M %p")


def _approver_display_name(decided_by):
    """The approver's real name for the email, not their login/username --
    decided_by is a res.users record, so plain .name gives the login-based
    display name (e.g. "Susu") rather than the linked hr.employee's actual
    full name (e.g. "Nyein Su Su"). Falls back to the user's own name for
    an account with no linked employee record."""
    if not decided_by:
        return "Any HR/Admin"
    return decided_by.employee_id.name or decided_by.name


def _render_approval_history(current_step, all_steps):
    """Steps already approved before the one this email is about, so an
    approver further down the chain can see who signed off before them and
    when -- only shown once there IS prior history (never on the very
    first step's own notification)."""
    prior_approved = all_steps.filtered(
        lambda s: s.state == "approved" and s.step_order < current_step.step_order
    ).sorted("step_order")
    if not prior_approved:
        return ""

    lines = [
        '<div style="margin:4px 0;font-size:13px;">%s (%s) approved at %s.</div>'
        % (_esc(s.label), _esc(_approver_display_name(s.decided_by)),
           _esc(_format_decided_at(s.decided_at)))
        for s in prior_approved
    ]
    return _approval_block("".join(lines))


def _render_final_approval_summary(all_steps):
    """Every decided step's outcome, for the requestor's final approved/
    rejected notification -- unlike _render_approval_history (only prior
    steps, always "approved", sent mid-chain), this covers the whole chain
    since the leave is now fully decided one way or the other."""
    decided = all_steps.filtered(lambda s: s.state in ("approved", "rejected")).sorted("step_order")
    if not decided:
        return ""

    lines = [
        '<div style="margin:4px 0;font-size:13px;">%s (%s) status(%s) at %s.</div>'
        % (_esc(s.label), _esc(_approver_display_name(s.decided_by)),
           _esc(s.state), _esc(_format_decided_at(s.decided_at)))
        for s in decided
    ]
    return _approval_block("".join(lines))


def _approval_block(lines_html):
    return (
        '<div style="margin-top:16px;">'
        '<div style="font-weight:700;font-size:14px;margin-bottom:6px;">Approval</div>'
        "%s"
        "</div>"
    ) % lines_html


def _analysis_row(flagged, label, message):
    color = "#d97706" if flagged else "#16a34a"
    icon = "⚠" if flagged else "✓"  # warning triangle / check mark
    return (
        '<div style="margin:4px 0;font-size:13px;">'
        '<span style="color:%s;font-weight:700;">%s</span> '
        '<strong>%s:</strong> %s'
        "</div>"
    ) % (color, icon, _esc(label), _esc(message))


def _analysis_block(rows_html):
    if not rows_html:
        return ""
    return (
        '<div style="margin-top:16px;">'
        '<div style="font-weight:700;font-size:14px;margin-bottom:6px;">Analysis</div>'
        "%s"
        "</div>"
    ) % rows_html


class HrLeave(models.Model):
    _inherit = "hr.leave"

    @api.model
    def _xeno_email_token(self, leave_id, step_id, action, user_id, exp):
        message = (leave_id, step_id, action, user_id, exp)
        return odoo_hmac(self.env(su=True), TOKEN_SCOPE, message)

    @api.model
    def _xeno_verify_email_token(self, leave_id, step_id, action, user_id, exp, token):
        """Constant-time check of an email action link. Deliberately NOT the
        same as XENHR's approveStepEmail (confirmed unauthenticated -- any
        link works for anyone): the token is bound to this exact leave,
        step, action and intended user, and expires, so it can't be
        replayed against a different record or reused past its window.
        Real authorization (is this user actually allowed to act on this
        step right now) still runs through the normal action_approve/
        action_refuse permission checks -- this only proves the link is
        genuine and hasn't been tampered with.
        """
        if not token or time.time() > exp:
            return False
        expected = self._xeno_email_token(leave_id, step_id, action, user_id, exp)
        return self._xeno_hmac_compare(expected, token)

    @api.model
    def _xeno_hmac_compare(self, a, b):
        import hmac as _hmac_lib
        return _hmac_lib.compare_digest(a, b)

    def _xeno_email_action_url(self, step, action, user_id, base_url=None):
        self.ensure_one()
        exp = int(time.time()) + TOKEN_VALID_SECONDS
        token = self._xeno_email_token(self.id, step.id, action, user_id, exp)
        base_url = base_url or self.env["ir.config_parameter"].sudo().get_param("web.base.url")
        return "%s/odoo/leave/email_action?leave_id=%s&step_id=%s&action=%s&user_id=%s&exp=%s&token=%s" % (
            base_url, self.id, step.id, action, user_id, exp, token)

    def _xeno_recent_leave_note(self):
        """Every OTHER approved leave for this employee within one month
        before this request's start date, each on its own line with its
        date range, duration and reason -- gives the approver a quick
        read on how often/recently this employee has already been out,
        not just the single most recent instance (the previous design).
        Returns pre-escaped safe HTML (each dynamic piece is escaped
        individually, then joined with a real <br/>) -- callers must NOT
        re-escape this like the other plain-text fields."""
        self.ensure_one()
        if not self.request_date_from:
            return _esc(_("None"))
        window_start = self.request_date_from - relativedelta(months=1)
        prior = self.sudo().search([
            ("employee_id", "=", self.employee_id.id),
            ("state", "=", "validate"),
            ("id", "!=", self.id),
            ("request_date_from", ">=", window_start),
            ("request_date_from", "<", self.request_date_from),
            # System deductions (Company Holiday / future Late Deduction)
            # aren't a self-requested absence -- exclude them here, same
            # as the Leave List page's default filtering.
            ("xeno_is_system_deduction", "=", False),
        ], order="request_date_from desc")
        if not prior:
            return _esc(_("None"))
        lines = []
        for leave in prior:
            date_range = str(leave.request_date_from)
            if leave.request_date_to and leave.request_date_to != leave.request_date_from:
                date_range += " – %s" % leave.request_date_to
            type_name = leave.holiday_status_id.name
            color = _leave_type_font_color(type_name)
            lines.append(
                '<span style="color:%s;font-weight:700;">%s</span>: %s (%s day(s)): %s'
                % (color, _esc(type_name), _esc(date_range), leave.number_of_days,
                   _esc(leave.sudo().name or "")))
        # Leading <br/> so the "Recent Leave Date:" label (added by the
        # _row_pair() caller) stays alone on its own line -- without it,
        # the first entry shares the label's line and only the rest wrap.
        return "<br/>" + "<br/>".join(lines)

    def _xeno_render_analysis_html(self):
        """Same 5-check advisory "Analysis" list shown in the Leave
        Approval decision dialog and the leave form's own HR/Admin-only
        sidebar widget (leave_analysis.js / leave_analysis_widget.js),
        ported to Python for the approver's action email -- kept in sync
        by hand since an email can't import a browser-side JS module.
        Thresholds (3/month, 4 in 90 days, 1.5x pace, <1 day remaining)
        match the web version exactly. Skipped entirely for a
        system-driven deduction (xeno_is_system_deduction), same as the
        two web-side call sites -- HR/the system chose the employee/type/
        days directly, so these self-service checks don't apply.
        """
        self.ensure_one()
        L = self.sudo()
        if L.xeno_is_system_deduction or not L.request_date_from:
            return ""

        date_from = L.request_date_from
        date_to = L.request_date_to or date_from
        total = L.xeno_balance_total or 0.0
        used = L.xeno_balance_used or 0.0
        pending = L.xeno_balance_pending or 0.0
        remaining = L.xeno_balance_remaining or 0.0
        requested = L.number_of_days or 0.0
        month_name = date_from.strftime("%B")

        rows = []

        # 1. How many requests already this calendar month (including this one).
        month_start = date_from.replace(day=1)
        month_end = month_start + relativedelta(months=1, days=-1)
        month_count = L.search_count([
            ("employee_id", "=", L.employee_id.id),
            ("state", "in", list(_ANALYSIS_ACTIVE_STATES)),
            ("request_date_from", ">=", month_start),
            ("request_date_from", "<=", month_end),
        ])
        rows.append(_analysis_row(
            month_count >= 3, "Monthly frequency",
            "%d leave request(s) in %s %d (including this one)." % (
                month_count, month_name, date_from.year)))

        # 2. Rolling 90-day frequency leading up to this request.
        window_start = date_from - timedelta(days=90)
        rolling_count = L.search_count([
            ("employee_id", "=", L.employee_id.id),
            ("state", "in", list(_ANALYSIS_ACTIVE_STATES)),
            ("request_date_from", ">=", window_start),
            ("request_date_from", "<=", date_from),
        ])
        rows.append(_analysis_row(
            rolling_count >= 4, "Recent request frequency",
            "%d leave request(s) in the 90 days leading up to this one." % rolling_count))

        # 3. Prorated pace: yearly allocation / 12 months, vs used+pending
        # so far this year (balance_pending already includes this request).
        if total > 0:
            monthly_rate = total / 12.0
            expected = monthly_rate * date_from.month
            actual = used + pending
            verdict = "Sufficient" if actual <= expected else "Insufficient"
            rows.append(_analysis_row(
                expected > 0 and actual > expected, "Prorated pace",
                "Prorated through %s: %.2f days · Used + Pending: %.2f days · %s" % (
                    month_name, expected, actual, verdict)))
        else:
            rows.append(_analysis_row(
                False, "Prorated pace",
                "No fixed yearly allocation for this leave type -- pace not applicable."))

        # 4. Remaining balance if this gets approved.
        if total > 0:
            after = remaining - requested
            rows.append(_analysis_row(
                after < 1, "Remaining after approval",
                "Approving this leaves %.2f day(s) of %s remaining this year." % (
                    after, L.holiday_status_id.name)))
        else:
            rows.append(_analysis_row(
                False, "Remaining after approval",
                "No fixed yearly allocation for this leave type -- balance not applicable."))

        # 5. Bridging pattern: a short request sitting right next to a
        # public holiday (resource.calendar.leaves with calendar_id=False
        # is company-wide/public, same data source as xeno_holidays).
        # Stored as naive UTC representing a Bangkok calendar day -- shift
        # +7h before taking .date(), same convention as
        # _format_decided_at above.
        day_before = date_from - timedelta(days=1)
        day_after = date_to + timedelta(days=1)
        holidays = self.env["resource.calendar.leaves"].sudo().search([
            ("calendar_id", "=", False),
            ("date_from", "<=", datetime.combine(day_after, dt_time.max)),
            ("date_to", ">=", datetime.combine(day_before, dt_time.min)),
        ])
        holiday_dates = {(h.date_from + timedelta(hours=7)).date() for h in holidays}
        is_short = 0 < requested <= 1
        bridges = is_short and (day_before in holiday_dates or day_after in holiday_dates)
        rows.append(_analysis_row(
            bridges, "Holiday-adjacent pattern",
            "This request falls immediately next to a public holiday (possible bridging)."
            if bridges else "Not adjacent to a public holiday."))

        return _analysis_block("".join(rows))

    def _xeno_step_email_recipients(self, step):
        """Specific approver if the step names one; otherwise every Time Off
        Officer (a blank approver_user_id means "any HR/Admin may act"), so
        each of them gets their own personalized, individually-tokened link.
        """
        if step.approver_user_id:
            return step.approver_user_id
        HR_OFFICER_GROUP = "hr_holidays.group_hr_holidays_user"
        # all_group_ids (not group_ids): group_ids only lists groups
        # directly/explicitly assigned, so a user who only holds a HIGHER
        # group that implies this one (e.g. the Manager-level access most
        # real HR staff here actually have) would be silently missed --
        # confirmed by testing against susu's own account, which holds
        # group_hr_holidays_manager (implies _user) but not _user directly.
        return self.env["res.users"].sudo().search([("all_group_ids", "in", [
            self.env.ref(HR_OFFICER_GROUP).id,
        ])])

    def _xeno_leave_email_fields(self, include_recent_leave=True):
        """Shared field block (employee/purpose/balance table/basis/total/
        dates/reason) used by both the pending-action email and the
        requestor's final-outcome email. include_recent_leave is only
        meaningful to an approver deciding whether to approve (susu's
        reference mockup for the final requestor notice omits it)."""
        self.ensure_one()
        L = self.sudo()
        emp = L.employee_id

        basis_label = _BASE_TYPE_LABELS.get(L.xeno_request_unit, "") if L.xeno_flexible_duration else ""
        is_part_time = L.xeno_request_unit == "part_time" and L.xeno_flexible_duration
        part_time_start = L.request_hour_from if is_part_time else None
        part_time_end = L.request_hour_to if is_part_time else None

        rows_before = [
            _row_pair(
                "พนักงานที่ขอลา (Employee)",
                "%s . %s" % (_esc(emp.sudo().xeno_employee_code or ""), _esc(emp.name))),
            _row_pair(
                "ความประสงค์ของการลา (Purpose of leave)",
                _esc(L.holiday_status_id.name)),
        ]

        table = """
        <table role="presentation" width="100%%" cellspacing="0" cellpadding="0"
               style="border-collapse:collapse;margin:14px 0;font-size:13px;">
          <tr>
            <th style="%(th)s">Leave entitled<br/>สิทธิ์ตามระเบียบบริษัทฯ</th>
            <th style="%(th)s">Accumulate of this year<br/>ยอดใช้ไปสะสมในรอบปี</th>
            <th style="%(th)s">Remaining<br/>สิทธิ์ที่เหลือ</th>
          </tr>
          <tr>
            <td style="%(td)s">%(allocated)s</td>
            <td style="%(td)s">%(used)s</td>
            <td style="%(td)s">%(remaining)s</td>
          </tr>
        </table>
        """ % {
            "th": "background:#e7edef;color:#3f5964;font-weight:600;padding:8px;border:1px solid #d8e1e4;text-align:center;font-size:11px;",
            "td": "padding:8px;border:1px solid #d8e1e4;text-align:center;font-variant-numeric:tabular-nums;",
            "allocated": "%.2f" % L.xeno_balance_total,
            "used": "%.2f" % L.xeno_balance_used,
            "remaining": "%.2f" % L.xeno_balance_remaining,
        }

        rows_after = []
        if basis_label:
            rows_after.append(_row_pair("ชนิดการลา (Base Type of leave)", _esc(basis_label)))
        rows_after.append(_row_pair("จำนวนวันลา (Total)", "%s day(s)" % L.number_of_days))
        rows_after.append(
            '<div style="margin:6px 0;font-size:13px;"><strong>วันที่เริ่มลา (Start date):</strong> %s'
            '&nbsp;&nbsp;&nbsp;<strong>วันสิ้นสุดลา (End date):</strong> %s</div>'
            % (_esc(str(L.request_date_from or "")), _esc(str(L.request_date_to or ""))))
        if is_part_time:
            rows_after.append(
                '<div style="margin:6px 0;font-size:13px;"><strong>จากเวลา (Start time):</strong> %s'
                '&nbsp;&nbsp;&nbsp;<strong>ถึงเวลา (Ending time):</strong> %s</div>'
                % (L._xeno_format_hour(part_time_start or 0.0), L._xeno_format_hour(part_time_end or 0.0)))
        rows_after.append(_row_pair("เหตุผลในการลา (Reason)", _esc(L.sudo().name or "")))
        if include_recent_leave:
            # _xeno_recent_leave_note already returns pre-escaped safe
            # HTML (its own <br/>-joined list) -- do not re-_esc() it,
            # unlike every other plain-text field above.
            rows_after.append(_row_pair("Recent Leave Date", L._xeno_recent_leave_note()))

        return {
            "rows_before": "".join(rows_before),
            "table": table,
            "rows_after": "".join(rows_after),
        }

    def _xeno_see_more_url(self, base_url=None):
        """Deep-links straight to this leave's own record (My Team Leaves
        action + this leave's id), so both the requestor and the approver
        land directly on the request itself instead of a generic list
        they'd then have to search through."""
        self.ensure_one()
        base_url = base_url or self.env["ir.config_parameter"].sudo().get_param("web.base.url")
        action_id = self.env.ref("xeno_leave.action_xeno_my_team_leaves").id
        return "%s/odoo/action-%s/%s" % (base_url, action_id, self.id)

    def _xeno_render_step_email_html(self, step, user):
        self.ensure_one()
        L = self.sudo()
        base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
        see_more_url = L._xeno_see_more_url(base_url)
        approve_url = L._xeno_email_action_url(step, "approve", user.id, base_url)
        reject_url = L._xeno_email_action_url(step, "reject", user.id, base_url)

        fields = L._xeno_leave_email_fields()
        status_label = dict(L._fields["state"].selection).get(L.state, L.state)
        history_html = _render_approval_history(step, L.xeno_approval_step_ids)
        analysis_html = L._xeno_render_analysis_html()

        body = (_HEADER % {
            "see_more": see_more_url,
            "status": _esc(status_label),
            "history": history_html,
            "analysis": analysis_html,
            **fields,
        }) + ("""
    <hr style="border:none;border-top:1px solid #d8e1e4;margin:16px 0;"/>
    <div style="text-align:center;font-size:13px;margin-bottom:14px;">
      ท่านมีใบลาเพื่อรอการอนุมัติดังนี้
      (You have request for absent as following for approval)
    </div>
    <div style="text-align:center;">
      <a href="%(approve)s" style="display:inline-block;background:#dff2e6;color:#1f7a4d;font-weight:700;padding:8px 22px;border-radius:16px;text-decoration:none;margin:0 6px;">Approve</a>
      <a href="%(reject)s" style="display:inline-block;background:#fbdad9;color:#b21f22;font-weight:700;padding:8px 22px;border-radius:16px;text-decoration:none;margin:0 6px;">Reject</a>
    </div>
""" % {"approve": approve_url, "reject": reject_url}) + _FOOTER

        return body

    def _xeno_render_final_notice_html(self):
        """The requestor's final-outcome notification -- same layout as the
        approver's action email, but read-only (no Approve/Reject links)
        and the Approval section covers every decided step's outcome
        (approved/rejected), not just the ones before a given step."""
        self.ensure_one()
        L = self.sudo()
        base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
        see_more_url = L._xeno_see_more_url(base_url)

        fields = L._xeno_leave_email_fields(include_recent_leave=False)
        status_label = dict(L._fields["state"].selection).get(L.state, L.state)
        history_html = _render_final_approval_summary(L.xeno_approval_step_ids)

        return (_HEADER % {
            "see_more": see_more_url,
            "status": _esc(status_label),
            "history": history_html,
            # Analysis is only for whoever is deciding the request (the
            # approver's action email, above) -- this is the requestor's
            # own read-only notice, decision already made, nothing to
            # weigh, so it's intentionally left blank here.
            "analysis": "",
            **fields,
        }) + _FOOTER

    def _xeno_send_step_email(self, step):
        """Email whoever can currently act on this step -- the specific
        approver, or every Time Off Officer for a blank (any-HR) step, each
        with their own individually-tokened Approve/Reject links."""
        self.ensure_one()
        recipients = self._xeno_step_email_recipients(step)
        for user in recipients:
            if not user.email:
                continue
            html = self._xeno_render_step_email_html(step, user)
            self.env["mail.mail"].sudo().create({
                "subject": "Employee Leave Request – %s (%s)" % (
                    self.employee_id.name, self.holiday_status_id.name),
                "body_html": html,
                "email_to": user.email,
                "auto_delete": True,
            }).send()

    def _xeno_send_final_notice(self):
        """Notify the requestor once their leave is fully approved or
        rejected -- read-only summary, no action links."""
        for leave in self:
            requestor = leave.employee_id.user_id
            if not requestor or not requestor.email:
                continue
            html = leave._xeno_render_final_notice_html()
            status_word = "Approved" if leave.state == "validate" else "Rejected"
            self.env["mail.mail"].sudo().create({
                "subject": "Employee Leave Request %s – %s (%s)" % (
                    status_word, leave.employee_id.name, leave.holiday_status_id.name),
                "body_html": html,
                "email_to": requestor.email,
                "auto_delete": True,
            }).send()
