import math

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


class XenoAttendanceLocation(models.Model):
    _name = "xeno.attendance.location"
    _description = "Allowed GPS check-in location (geofence), mirrors XENHR's locations table"
    _order = "name"

    name = fields.Char(required=True)
    latitude = fields.Float(digits=(10, 7), required=True)
    longitude = fields.Float(digits=(10, 7), required=True)
    radius = fields.Integer(
        string="Radius (meters)", required=True, default=100,
        help="Allowed distance from this point, in meters.")
    active = fields.Boolean(default=True)
    remark = fields.Char()

    def action_xeno_preview(self):
        """Opens a small map dialog (Leaflet/OpenStreetMap) showing this
        location's marker and geofence radius -- lets HR sanity-check a
        pinned coordinate without having to open OpenStreetMap separately."""
        self.ensure_one()
        return {
            "type": "ir.actions.client",
            "tag": "xeno_attendance_location_preview",
            "name": self.name,
            "target": "new",
            "params": {"location_id": self.id},
        }

    _radius_positive = models.Constraint(
        "CHECK(radius > 0)", "Radius must be greater than 0.",
    )

    @staticmethod
    def _xeno_haversine(lat1, lon1, lat2, lon2):
        """Distance in meters between two GPS points -- same formula as
        XENHR's AttendanceService::haversineDistance."""
        earth_radius = 6371000
        d_lat = math.radians(lat2 - lat1)
        d_lon = math.radians(lon2 - lon1)
        a = (math.sin(d_lat / 2) ** 2
             + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(d_lon / 2) ** 2)
        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
        return earth_radius * c

    def _xeno_match(self, latitude, longitude):
        """Among these (active) locations, return (matched_location, min_distance)
        -- matched_location is the closest one the point falls within, or an
        empty recordset if none match (min_distance is still the closest
        distance found, for a useful error message)."""
        min_distance = None
        for location in self:
            distance = self._xeno_haversine(latitude, longitude, location.latitude, location.longitude)
            if min_distance is None or distance < min_distance:
                min_distance = distance
            if distance <= location.radius:
                return location, distance
        return self.browse(), (min_distance or 0.0)


class HrAttendance(models.Model):
    _inherit = "hr.attendance"

    xeno_location_id = fields.Many2one(
        "xeno.attendance.location", string="Matched Location", readonly=True,
        help="Which configured geofence this check-in matched, if any.")


class HrEmployee(models.Model):
    _inherit = "hr.employee"

    def _attendance_action_change(self, geo_information=None):
        """Geofence GPS check-ins against xeno.attendance.location, mirroring
        XENHR's AttendanceService::checkIn/validateGpsLocation exactly: if no
        active locations are configured, no restriction (today's behavior,
        unchanged); otherwise GPS coordinates are required and must fall
        within at least one location's radius, or the check-in is rejected
        with the closest distance reported -- same as XENHR's
        OutsideLocationException. Only applies to check-IN (XENHR never
        geofences check-out either).

        Only applies to 'systray' check-ins (an employee's own device) --
        never to 'kiosk' (a shared, physically-controlled terminal at a
        fixed office location, where core sets geo_information['mode'] to
        'kiosk'). A kiosk's own physical presence already proves the
        workplace; requiring browser GPS from it as well is both pointless
        and often impossible in practice (geolocation is blocked by
        browsers on non-HTTPS origins, and GeoIP can't resolve a private
        LAN address), so enforcing it here would just lock out the kiosk
        entirely once any geofence location is configured.
        """
        is_checking_in = self.attendance_state != "checked_in"
        matched = None
        if is_checking_in and (geo_information or {}).get("mode") != "kiosk":
            locations = self.env["xeno.attendance.location"].sudo().search([("active", "=", True)])
            if locations:
                latitude = (geo_information or {}).get("latitude")
                longitude = (geo_information or {}).get("longitude")
                if not latitude or not longitude:
                    raise UserError(_(
                        "GPS coordinates are required to check in at this "
                        "workplace. Please enable location access and try "
                        "again."))
                matched, min_distance = locations._xeno_match(latitude, longitude)
                if not matched:
                    raise UserError(_(
                        "You are not within any designated check-in area "
                        "(closest is %(distance)s m away). Please check your "
                        "location and try again.",
                        distance=int(min_distance)))
        attendance = super()._attendance_action_change(geo_information=geo_information)
        if matched:
            attendance.sudo().xeno_location_id = matched.id
        return attendance
