import datetime
import logging
import re

from odoo import _, http
from odoo.exceptions import AccessError, UserError
from odoo.http import request

_logger = logging.getLogger(__name__)

TIME_RE = re.compile(r"^([01]\d|2[0-3]):[0-5]\d$")


class XenoAttendanceController(http.Controller):

    def _has_access(self):
        user = request.env.user
        # Attendance carries employee PII — gate to attendance officers or HR
        # officers (parity with XENHR's HR-only report page, and matches the
        # group the Reporting menu item is shown to).
        return (user.has_group("hr_attendance.group_hr_attendance_officer")
                or user.has_group("hr.group_hr_user"))

    def _can_edit(self):
        # Overriding a scanned punch is a stricter action than viewing the
        # report — only Attendance Officers, matching the Officer-only gate
        # on xeno_leave's force-approve/reject.
        return request.env.user.has_group("hr_attendance.group_hr_attendance_officer")

    def _team_codes(self):
        return request.env["xeno.attendance.viewer"].sudo().get_team_codes(
            request.env.user)

    def _forbidden(self):
        return request.make_response(
            "<h3 style='font-family:sans-serif;padding:2rem'>403 — %s</h3>"
            % _("HR access required to view attendance."),
            headers=[("Content-Type", "text/html; charset=utf-8")],
            status=403,
        )

    def _parse_year_month(self, year, month):
        today = datetime.date.today()
        try:
            year = int(year) if year else today.year
        except (TypeError, ValueError):
            year = today.year
        try:
            month = int(month) if month else today.month
        except (TypeError, ValueError):
            month = today.month
        return year, min(max(month, 1), 12)

    @http.route(
        "/odoo/attendances/bymonth",
        type="http", auth="user", website=False, sitemap=False, csrf=False,
    )
    def bymonth(self, year=None, month=None, employee=None,
                department=None, group=None, position=None, **kw):
        if not self._has_access():
            return self._forbidden()
        year, month = self._parse_year_month(year, month)
        Report = request.env["xeno.attendance.report"].sudo()
        filter_codes = Report.xeno_codes_for_filters(department, group, position)

        try:
            html = Report.render_html(
                year, month, employee or None, restrict_codes=filter_codes,
                can_edit=self._can_edit(),
                department_id=department or None, category_id=group or None,
                job_id=position or None,
            )
        except Exception as exc:  # surface a readable error instead of a 500 page
            _logger.exception("attendance bymonth render failed")
            html = (
                "<div style='font-family:sans-serif;padding:2rem;color:#b21f22'>"
                "<h3>%s</h3>"
                "<p>%s</p>"
                "<pre style='white-space:pre-wrap;color:#555'>%s</pre></div>"
                % (
                    _("Attendance report unavailable"),
                    _("Could not build the report from HRSystem."),
                    request.env["xeno.attendance.report"]._esc(str(exc)),
                )
            )
        return request.make_response(
            html, headers=[("Content-Type", "text/html; charset=utf-8")]
        )

    @http.route(
        "/odoo/attendances/bymonth/export",
        type="http", auth="user", website=False, sitemap=False, csrf=False,
    )
    def bymonth_export(self, year=None, month=None, employee=None,
                       department=None, group=None, position=None, **kw):
        if not self._has_access():
            return self._forbidden()
        year, month = self._parse_year_month(year, month)
        Report = request.env["xeno.attendance.report"].sudo()
        filter_codes = Report.xeno_codes_for_filters(department, group, position)

        try:
            xlsx = Report.export_xlsx(
                year, month, employee or None, restrict_codes=filter_codes
            )
        except Exception:
            _logger.exception("attendance bymonth export failed")
            return request.make_response(
                _("Export failed — check server logs."), status=500)

        filename = "attendance_%04d_%02d.xlsx" % (year, month)
        return request.make_response(
            xlsx,
            headers=[
                ("Content-Type",
                 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
                ("Content-Disposition", 'attachment; filename="%s"' % filename),
            ],
        )

    @http.route(
        "/odoo/attendances/bymonth/edit",
        type="json", auth="user",
    )
    def bymonth_edit(self, employee_code=None, date=None, field=None, value=None, **kw):
        if not self._can_edit():
            raise AccessError(_("Attendance Officer access required to edit attendance."))
        if field not in ("in", "out"):
            raise UserError(_("Invalid field."))
        value = (value or "").strip()
        if value and not TIME_RE.match(value):
            raise UserError(_("Time must be in HH:MM 24-hour format."))
        try:
            date_obj = datetime.datetime.strptime(date, "%Y-%m-%d").date()
        except (TypeError, ValueError):
            raise UserError(_("Invalid date."))

        Override = request.env["xeno.attendance.override"].sudo()
        vals_field = "time_in" if field == "in" else "time_out"
        other_field = "time_out" if field == "in" else "time_in"
        rec = Override.search([
            ("employee_code", "=", str(employee_code)),
            ("date", "=", date_obj),
        ], limit=1)
        if rec:
            rec.write({vals_field: value or False})
            if not rec[vals_field] and not rec[other_field]:
                rec.unlink()
        elif value:
            Override.create({
                "employee_code": str(employee_code),
                "date": date_obj,
                vals_field: value,
            })
        return {"ok": True}

    def _my_code(self):
        # xeno_employee_code carries groups="hr.group_hr_user", so a regular
        # employee can't read it on their own record — resolve it via sudo.
        # Only the caller's own linked employee is ever consulted.
        emp = request.env.user.employee_id
        if not emp:
            emp = request.env["hr.employee"].sudo().search(
                [("user_id", "=", request.env.user.id)], limit=1)
        return emp and emp.sudo().xeno_employee_code or False

    def _not_linked(self):
        return request.make_response(
            "<div style='font-family:sans-serif;padding:2rem'>"
            "<h3>My Attendance is not available yet</h3>"
            "<p>Your login is not linked to an employee record with an "
            "HRSystem employee code. Please contact HR.</p></div>",
            headers=[("Content-Type", "text/html; charset=utf-8")],
        )

    @http.route(
        "/odoo/attendances/my",
        type="http", auth="user", website=False, sitemap=False, csrf=False,
    )
    def my_attendance(self, year=None, month=None, **kw):
        code = self._my_code()
        if not code:
            return self._not_linked()
        year, month = self._parse_year_month(year, month)

        try:
            html = request.env["xeno.attendance.report"].sudo().render_html(
                year, month, None, restrict_codes=[code],
                base_url="/odoo/attendances/my", my_mode=True,
            )
        except Exception as exc:
            _logger.exception("attendance my render failed")
            html = (
                "<div style='font-family:sans-serif;padding:2rem;color:#b21f22'>"
                "<h3>My Attendance unavailable</h3>"
                "<p>Could not build the report from HRSystem.</p>"
                "<pre style='white-space:pre-wrap;color:#555'>%s</pre></div>"
                % request.env["xeno.attendance.report"]._esc(str(exc))
            )
        return request.make_response(
            html, headers=[("Content-Type", "text/html; charset=utf-8")]
        )

    @http.route(
        "/odoo/attendances/my/export",
        type="http", auth="user", website=False, sitemap=False, csrf=False,
    )
    def my_attendance_export(self, year=None, month=None, **kw):
        code = self._my_code()
        if not code:
            return self._not_linked()
        year, month = self._parse_year_month(year, month)

        try:
            xlsx = request.env["xeno.attendance.report"].sudo().export_xlsx(
                year, month, None, restrict_codes=[code],
            )
        except Exception:
            _logger.exception("attendance my export failed")
            return request.make_response(
                _("Export failed — check server logs."), status=500)

        filename = "my_attendance_%04d_%02d.xlsx" % (year, month)
        return request.make_response(
            xlsx,
            headers=[
                ("Content-Type",
                 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
                ("Content-Disposition", 'attachment; filename="%s"' % filename),
            ],
        )

    def _my_employee(self):
        employee = request.env.user.employee_id
        if not employee:
            employee = request.env["hr.employee"].sudo().search(
                [("user_id", "=", request.env.user.id)], limit=1)
        return employee

    @http.route(
        "/odoo/attendances/my/today_status",
        type="json", auth="user",
    )
    def my_today_status(self, **kw):
        """Today's merged first-in/last-out for the calling user's own
        employee -- lets the My Attendance widget notice a fingerprint
        check-in that Odoo's own hr_attendance module doesn't know about,
        without pulling the whole month.
        """
        code = self._my_code()
        if not code:
            return {"in": None, "out": None}
        return request.env["xeno.attendance.report"].sudo().get_today_status(code)

    @http.route(
        "/odoo/attendances/my/month_present_count",
        type="json", auth="user",
    )
    def my_month_present_count(self, year=None, month=None, **kw):
        """This calendar month's distinct present-days for the calling
        user's own employee, from the same merged biometric+native source
        as the My Attendance report -- used by Odoo Mobile's home page
        "This Month / Days Present" card, which used to count raw
        hr.attendance rows only and silently showed 0 for anyone who
        exclusively uses the physical biometric scanner.
        """
        code = self._my_code()
        if not code:
            return {"present": 0}
        today = datetime.datetime.now()
        y = int(year) if year else today.year
        m = int(month) if month else today.month
        count = request.env["xeno.attendance.report"].sudo().get_month_present_count(code, y, m)
        return {"present": count}

    @http.route(
        "/odoo/attendances/my/close_fingerprint_day",
        type="json", auth="user",
    )
    def my_close_fingerprint_day(self, **kw):
        """Explicit "Check Out" for a day that started with a
        fingerprint-only check-in: writes a real hr.attendance record
        (check_in = the fingerprint's scan time, check_out = now) so Odoo's
        own attendance state becomes consistent going forward.
        """
        employee = self._my_employee()
        if not employee:
            raise UserError(_("No linked employee record found."))
        return request.env["xeno.attendance.report"].sudo().close_fingerprint_day(employee.id)

    @http.route(
        "/odoo/attendances/my/native_status",
        type="json", auth="user",
    )
    def my_native_status(self, **kw):
        """Whether the calling user has a currently open native
        hr.attendance check-in, and whether it's stale (open from a
        previous day/shift, effectively a forgotten checkout) -- lets the
        My Attendance widget show Absent/Check In for today instead of an
        indefinitely-stale Present.
        """
        employee = self._my_employee()
        if not employee:
            return {"open": False, "stale": False}
        return request.env["xeno.attendance.report"].sudo().get_native_checkin_status(employee.id)

    @http.route(
        "/odoo/attendances/my/close_stale_checkin",
        type="json", auth="user",
    )
    def my_close_stale_checkin(self, **kw):
        """Auto-close a forgotten open check-in (see my_native_status) so
        the employee can check in fresh today. Closes at 23:59:59 on the
        stale check-in's own day -- the actual fresh check-in is then done
        by the normal core check-in flow right after, from the client.
        """
        employee = self._my_employee()
        if not employee:
            raise UserError(_("No linked employee record found."))
        return request.env["xeno.attendance.report"].sudo().close_stale_checkin(employee.id)

    @http.route(
        "/odoo/attendances/my/calendar",
        type="jsonrpc", auth="user",
    )
    def my_attendance_calendar(self, year=None, month=None, **kw):
        """Slim per-day attendance summary for the My Profile dashboard
        widget's mini-calendar -- same HRSystem source as /odoo/attendances/my
        (biometric + app/GPS punches), just shaped for a calendar cell
        instead of a full grid. {"linked": False} if this login isn't
        bridged to an HRSystem employee_code yet.
        """
        code = self._my_code()
        if not code:
            return {"linked": False}
        year, month = self._parse_year_month(year, month)

        Report = request.env["xeno.attendance.report"].sudo()
        data = Report.get_month_report(year, month, None, restrict_codes=[code])
        row = data["rows"][0] if data["rows"] else None
        today_str = datetime.date.today().isoformat()
        auto_fill = code in request.env["xeno.attendance.auto.rule"].sudo().get_auto_fill_codes()

        days = {}
        for d in range(1, data["days"] + 1):
            datestr = "%04d-%02d-%02d" % (year, month, d)
            is_weekend = datetime.date(year, month, d).weekday() >= 5
            is_holiday = datestr in data["holidays"]
            on_leave = next(
                (lv["code"] for lv in data["leaves"]
                 if lv["start"] <= datestr <= lv["end"]
                 and (not row or lv["employee_id"] == row.get("employee_id"))),
                None,
            )
            on_dayoff = (code, datestr) in data.get("dayoffs", {})
            on_bt = (code, datestr) in data.get("business_trips", {})
            tin = Report._fmt(row.get("%s.In" % datestr)) if row else None
            tout = Report._fmt(row.get("%s.Out" % datestr)) if row else None
            sstart = ((row.get("shift_start") or "09:00")[:5]) if row else "09:00"

            if on_bt:
                status = "bt"
            elif on_dayoff:
                status = "off"
            elif is_holiday:
                status = "holiday"
            elif is_weekend:
                status = "weekend"
            elif on_leave:
                status = "leave"
            elif tin:
                status = "late" if tin > sstart else "present"
            elif auto_fill:
                status = "present"
            elif datestr < today_str:
                status = "absent"
            else:
                status = "upcoming"

            days[datestr] = {"status": status, "in": tin, "out": tout, "leave_code": on_leave}

        return {"linked": True, "days": days}

    @http.route(
        "/odoo/attendances/myteam",
        type="http", auth="user", website=False, sitemap=False, csrf=False,
    )
    def myteam(self, year=None, month=None, employee=None, **kw):
        team_codes = self._team_codes()
        if not self._has_access() and not team_codes:
            return self._forbidden()
        year, month = self._parse_year_month(year, month)

        try:
            html = request.env["xeno.attendance.report"].sudo().render_html(
                year, month, employee or None, restrict_codes=team_codes,
                base_url="/odoo/attendances/myteam", team_mode=True,
            )
        except Exception as exc:
            _logger.exception("attendance myteam render failed")
            html = (
                "<div style='font-family:sans-serif;padding:2rem;color:#b21f22'>"
                "<h3>My Team Attendance unavailable</h3>"
                "<p>Could not build the report from HRSystem.</p>"
                "<pre style='white-space:pre-wrap;color:#555'>%s</pre></div>"
                % request.env["xeno.attendance.report"]._esc(str(exc))
            )
        return request.make_response(
            html, headers=[("Content-Type", "text/html; charset=utf-8")]
        )

    @http.route(
        "/odoo/attendances/myteam/export",
        type="http", auth="user", website=False, sitemap=False, csrf=False,
    )
    def myteam_export(self, year=None, month=None, employee=None, **kw):
        team_codes = self._team_codes()
        if not self._has_access() and not team_codes:
            return self._forbidden()
        year, month = self._parse_year_month(year, month)

        try:
            xlsx = request.env["xeno.attendance.report"].sudo().export_xlsx(
                year, month, employee or None, restrict_codes=team_codes,
            )
        except Exception:
            _logger.exception("attendance myteam export failed")
            return request.make_response(
                _("Export failed — check server logs."), status=500)

        filename = "my_team_attendance_%04d_%02d.xlsx" % (year, month)
        return request.make_response(
            xlsx,
            headers=[
                ("Content-Type",
                 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
                ("Content-Disposition", 'attachment; filename="%s"' % filename),
            ],
        )
