# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)

import base64
import math
import logging
from datetime import datetime, timedelta
from odoo import http, fields, _
from odoo.http import request

_logger = logging.getLogger(__name__)

STREAK_MILESTONES = [5, 10, 20, 50, 100]


def haversine_distance(lat1, lon1, lat2, lon2):
    """Calculate the great-circle distance between two points on Earth (in meters)."""
    R = 6371000
    phi1 = math.radians(lat1)
    phi2 = math.radians(lat2)
    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)

    a = (math.sin(delta_phi / 2) ** 2
         + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2)
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return R * c


class FusionClockAPI(http.Controller):
    """JSON API endpoints for Fusion Clock operations."""

    def _get_employee(self):
        user = request.env.user
        return request.env['hr.employee'].sudo().search([
            ('user_id', '=', user.id),
        ], limit=1)

    def _get_locations_for_employee(self, employee):
        Location = request.env['fusion.clock.location'].sudo()
        locations = Location.search([
            ('active', '=', True),
            ('company_id', '=', employee.company_id.id),
        ])
        return locations.filtered(
            lambda loc: loc.all_employees or employee.id in loc.employee_ids.ids
        )

    def _verify_location(self, latitude, longitude, employee, client_ip=None):
        """Verify GPS coordinates or IP against allowed geofences.
        Returns (location_record, distance, error_detail, method) tuple.
        method is 'gps' or 'ip'.
        """
        locations = self._get_locations_for_employee(employee)

        if not locations:
            return False, 0, 'no_locations', 'gps'

        gps_available = latitude != 0 or longitude != 0

        geocoded = locations.filtered(lambda l: l.latitude and l.longitude
                                       and not (l.latitude == 0.0 and l.longitude == 0.0))

        nearest_distance = float('inf')

        if gps_available and geocoded:
            for loc in geocoded:
                dist = haversine_distance(latitude, longitude, loc.latitude, loc.longitude)
                if dist <= loc.radius:
                    return loc, dist, None, 'gps'
                if dist < nearest_distance:
                    nearest_distance = dist

        # IP fallback -- try when GPS is unavailable OR GPS is outside all geofences
        ICP = request.env['ir.config_parameter'].sudo()
        if client_ip:
            for loc in locations:
                if loc.check_ip_whitelist(client_ip):
                    return loc, 0, None, 'ip'

        if not gps_available:
            return False, 0, 'gps_unavailable', 'gps'

        if not geocoded:
            return False, 0, 'no_geocoded', 'gps'

        return False, nearest_distance, 'outside', 'gps'

    def _location_error_message(self, error_type, distance=0):
        if error_type == 'no_locations':
            return 'No clock locations configured. Ask your manager to set up locations in Fusion Clock > Locations.'
        elif error_type == 'no_geocoded':
            return 'Clock locations exist but have no GPS coordinates. Ask your manager to geocode them.'
        elif error_type == 'gps_unavailable':
            return 'Could not determine your location. Please enable GPS/location services in your browser and device settings, then try again.'
        else:
            dist_str = f"{int(distance)}m" if distance < 1000 else f"{distance/1000:.1f}km"
            return f'You are {dist_str} away from the nearest clock location. Please clock in/out within the allowed area.'

    def _get_scheduled_times(self, employee, date):
        """Get scheduled clock-in and clock-out datetime using employee shift."""
        return employee._get_fclk_scheduled_times(date)

    def _check_and_create_penalty(self, employee, attendance, penalty_type, scheduled_dt, actual_dt):
        """Check if a penalty should be created and deduct minutes."""
        ICP = request.env['ir.config_parameter'].sudo()
        if ICP.get_param('fusion_clock.enable_penalties', 'True') != 'True':
            return

        grace = float(ICP.get_param('fusion_clock.penalty_grace_minutes', '5'))
        deduction = float(ICP.get_param('fusion_clock.penalty_deduction_minutes', '15'))
        diff_minutes = abs((actual_dt - scheduled_dt).total_seconds()) / 60.0

        should_penalize = False
        if penalty_type == 'late_in' and actual_dt > scheduled_dt:
            should_penalize = diff_minutes > grace
        elif penalty_type == 'early_out' and actual_dt < scheduled_dt:
            should_penalize = diff_minutes > grace

        if should_penalize:
            request.env['fusion.clock.penalty'].sudo().create({
                'attendance_id': attendance.id,
                'employee_id': employee.id,
                'penalty_type': penalty_type,
                'scheduled_time': scheduled_dt,
                'actual_time': actual_dt,
                'penalty_minutes': deduction,
                'date': actual_dt.date() if isinstance(actual_dt, datetime) else fields.Date.today(),
            })

            # Deduct penalty minutes from attendance (adds to break deduction)
            current_break = attendance.x_fclk_break_minutes or 0.0
            attendance.sudo().write({
                'x_fclk_break_minutes': current_break + deduction,
            })

            # Log penalty
            log_type = 'late_clock_in' if penalty_type == 'late_in' else 'early_clock_out'
            request.env['fusion.clock.activity.log'].sudo().create({
                'employee_id': employee.id,
                'log_type': log_type,
                'description': f"{'Late' if penalty_type == 'late_in' else 'Early'} by "
                               f"{diff_minutes:.0f} min. {deduction:.0f} min deducted.",
                'attendance_id': attendance.id,
                'source': 'system',
            })

            # Reset streak on late clock-in
            if penalty_type == 'late_in':
                employee.sudo().write({'x_fclk_ontime_streak': 0})

    def _apply_break_deduction(self, attendance, employee):
        """Apply automatic break deduction if configured."""
        ICP = request.env['ir.config_parameter'].sudo()
        if ICP.get_param('fusion_clock.auto_deduct_break', 'True') != 'True':
            return

        threshold = float(ICP.get_param('fusion_clock.break_threshold_hours', '4.0'))
        worked = attendance.worked_hours or 0.0

        if worked >= threshold:
            break_min = employee._get_fclk_break_minutes()
            current = attendance.x_fclk_break_minutes or 0.0
            # Set to whichever is higher: configured break or existing (penalty-inflated) value
            new_val = max(break_min, current)
            if new_val != current:
                attendance.sudo().write({'x_fclk_break_minutes': new_val})

    def _log_activity(self, employee, log_type, description, attendance=None,
                      location=None, latitude=0, longitude=0, distance=0, source='portal'):
        """Create an activity log entry."""
        try:
            request.env['fusion.clock.activity.log'].sudo().create({
                'employee_id': employee.id,
                'log_type': log_type,
                'description': description,
                'attendance_id': attendance.id if attendance else False,
                'location_id': location.id if location else False,
                'latitude': latitude,
                'longitude': longitude,
                'distance': distance,
                'source': source,
            })
        except Exception as e:
            _logger.error("Fusion Clock: Failed to create activity log: %s", e)

    # -------------------------------------------------------------------------
    # API Endpoints
    # -------------------------------------------------------------------------

    @http.route('/fusion_clock/verify_location', type='jsonrpc', auth='user', methods=['POST'])
    def verify_location(self, latitude=0, longitude=0, accuracy=0, **kw):
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        client_ip = request.httprequest.environ.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
        if not client_ip:
            client_ip = request.httprequest.remote_addr

        location, distance, err, method = self._verify_location(latitude, longitude, employee, client_ip)

        if location:
            return {
                'allowed': True,
                'location_id': location.id,
                'location_name': location.name,
                'location_address': location.address or '',
                'distance': round(distance, 1),
                'radius': location.radius,
                'method': method,
            }
        else:
            msg = self._location_error_message(err, distance)
            return {
                'allowed': False,
                'nearest_distance': round(distance, 1) if distance != float('inf') else None,
                'message': msg,
                'error_type': err,
            }

    @http.route('/fusion_clock/clock_action', type='jsonrpc', auth='user', methods=['POST'])
    def clock_action(self, latitude=0, longitude=0, accuracy=0, source='portal', photo=None, **kw):
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        if not employee.x_fclk_enable_clock:
            return {'error': 'Fusion Clock is not enabled for your account.'}

        # Check pending reason before clock-in
        is_checked_in = employee.attendance_state == 'checked_in'
        if not is_checked_in and employee.x_fclk_pending_reason:
            return {
                'requires_reason': True,
                'message': 'Please provide a reason for your missed clock-out before clocking in.',
            }

        # Server-side location verification
        client_ip = request.httprequest.environ.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
        if not client_ip:
            client_ip = request.httprequest.remote_addr

        location, distance, err, method = self._verify_location(latitude, longitude, employee, client_ip)
        if not location:
            err_msg = self._location_error_message(err, distance)
            err_msg += f" (Your IP: {client_ip})"
            self._log_activity(
                employee, 'outside_geofence',
                err_msg,
                latitude=latitude, longitude=longitude, distance=distance, source=source,
            )
            return {
                'error': err_msg,
                'allowed': False,
                'error_type': err,
            }

        # Log IP fallback usage
        if method == 'ip':
            self._log_activity(
                employee, 'ip_fallback',
                f"IP-based verification used (IP: {client_ip}) at {location.name}",
                location=location, source=source,
            )

        now = fields.Datetime.now()
        today = now.date()

        geo_info = {
            'latitude': latitude,
            'longitude': longitude,
            'browser': kw.get('browser', ''),
            'ip_address': client_ip,
        }

        ICP = request.env['ir.config_parameter'].sudo()

        try:
            if not is_checked_in:
                # CLOCK IN
                attendance = employee.sudo()._attendance_action_change(geo_info)

                write_vals = {
                    'x_fclk_location_id': location.id,
                    'x_fclk_in_distance': round(distance, 1),
                    'x_fclk_clock_source': source,
                }

                # Photo verification
                if photo and location.require_photo:
                    try:
                        write_vals['x_fclk_checkin_photo'] = photo
                    except Exception:
                        pass

                attendance.sudo().write(write_vals)

                # Log clock-in
                self._log_activity(
                    employee, 'clock_in',
                    f"Clocked in at {location.name} ({method.upper()})",
                    attendance=attendance, location=location,
                    latitude=latitude, longitude=longitude, distance=distance,
                    source=source,
                )

                # Check for late clock-in penalty
                scheduled_in, _ = self._get_scheduled_times(employee, today)
                self._check_and_create_penalty(employee, attendance, 'late_in', scheduled_in, now)

                # Update on-time streak
                diff_min = (now - scheduled_in).total_seconds() / 60.0 if now > scheduled_in else 0
                grace = float(ICP.get_param('fusion_clock.penalty_grace_minutes', '5'))
                if diff_min <= grace:
                    new_streak = (employee.x_fclk_ontime_streak or 0) + 1
                    employee.sudo().write({'x_fclk_ontime_streak': new_streak})
                    if new_streak in STREAK_MILESTONES:
                        self._log_activity(
                            employee, 'streak_milestone',
                            f"On-time streak reached {new_streak} days!",
                            attendance=attendance, source='system',
                        )

                # Very late notification
                very_late = float(ICP.get_param('fusion_clock.very_late_threshold_minutes', '15'))
                if diff_min > very_late:
                    office_user_id = int(ICP.get_param('fusion_clock.office_user_id', '0'))
                    if office_user_id:
                        request.env['hr.attendance'].sudo()._fclk_notify_office(
                            office_user_id,
                            f"Very Late Clock-In: {employee.name}",
                            f"{employee.name} clocked in {int(diff_min)} minutes late.",
                            'hr.attendance',
                            attendance.id,
                        )

                return {
                    'success': True,
                    'action': 'clock_in',
                    'attendance_id': attendance.id,
                    'check_in': fields.Datetime.to_string(attendance.check_in),
                    'location_name': location.name,
                    'message': f'Clocked in at {location.name}',
                    'streak': employee.x_fclk_ontime_streak,
                }

            else:
                # CLOCK OUT
                attendance = employee.sudo()._attendance_action_change(geo_info)
                attendance.sudo().write({
                    'x_fclk_out_distance': round(distance, 1),
                })

                # Apply break deduction
                self._apply_break_deduction(attendance, employee)

                # Check for early clock-out penalty
                _, scheduled_out = self._get_scheduled_times(employee, today)
                self._check_and_create_penalty(employee, attendance, 'early_out', scheduled_out, now)

                # Log clock-out
                self._log_activity(
                    employee, 'clock_out',
                    f"Clocked out from {location.name}. Net: {attendance.x_fclk_net_hours:.1f}h",
                    attendance=attendance, location=location,
                    latitude=latitude, longitude=longitude, distance=distance,
                    source=source,
                )

                # Log overtime
                if attendance.x_fclk_is_overtime and attendance.x_fclk_overtime_hours > 0:
                    self._log_activity(
                        employee, 'overtime',
                        f"Overtime: {attendance.x_fclk_overtime_hours:.1f}h beyond scheduled shift",
                        attendance=attendance, source='system',
                    )

                return {
                    'success': True,
                    'action': 'clock_out',
                    'attendance_id': attendance.id,
                    'check_in': fields.Datetime.to_string(attendance.check_in),
                    'check_out': fields.Datetime.to_string(attendance.check_out),
                    'worked_hours': round(attendance.worked_hours or 0, 2),
                    'net_hours': round(attendance.x_fclk_net_hours or 0, 2),
                    'break_minutes': attendance.x_fclk_break_minutes,
                    'overtime_hours': round(attendance.x_fclk_overtime_hours or 0, 2),
                    'location_name': location.name,
                    'message': f'Clocked out from {location.name}',
                }

        except Exception as e:
            _logger.error("Fusion Clock error: %s", str(e))
            return {'error': str(e)}

    @http.route('/fusion_clock/submit_reason', type='jsonrpc', auth='user', methods=['POST'])
    def submit_reason(self, reason='', departure_time='', **kw):
        """Submit a reason for missed clock-out."""
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        if not reason:
            return {'error': 'Please provide a reason.'}

        # Find the last unclosed attendance or last auto-closed
        last_att = request.env['hr.attendance'].sudo().search([
            ('employee_id', '=', employee.id),
            ('check_out', '!=', False),
        ], order='check_out desc', limit=1)

        if last_att and departure_time:
            try:
                dep_dt = fields.Datetime.from_string(departure_time)
                if dep_dt and dep_dt > last_att.check_in:
                    last_att.sudo().write({'check_out': dep_dt})
            except Exception:
                pass

        # Log the reason
        self._log_activity(
            employee, 'reason_provided',
            f"Reason for missed clock-out: {reason}. "
            f"Reported departure: {departure_time or 'not specified'}",
            attendance=last_att if last_att else None,
            source='portal',
        )

        employee.sudo().write({'x_fclk_pending_reason': False})

        return {'success': True, 'message': 'Reason submitted. You may now clock in.'}

    @http.route('/fusion_clock/request_leave', type='jsonrpc', auth='user', methods=['POST'])
    def request_leave(self, leave_date='', reason='', **kw):
        """Submit a leave request from the portal."""
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        if not leave_date or not reason:
            return {'error': 'Please provide both a date and a reason.'}

        try:
            date_obj = fields.Date.from_string(leave_date)
        except Exception:
            return {'error': 'Invalid date format. Use YYYY-MM-DD.'}

        existing = request.env['fusion.clock.leave.request'].sudo().search([
            ('employee_id', '=', employee.id),
            ('leave_date', '=', date_obj),
        ], limit=1)
        if existing:
            return {'error': 'A leave request already exists for this date.'}

        request.env['fusion.clock.leave.request'].sudo().create({
            'employee_id': employee.id,
            'leave_date': date_obj,
            'reason': reason,
            'created_from': 'portal',
        })

        return {'success': True, 'message': f'Leave request for {leave_date} submitted.'}

    @http.route('/fusion_clock/request_correction', type='jsonrpc', auth='user', methods=['POST'])
    def request_correction(self, attendance_id=0, check_in='', check_out='', reason='', **kw):
        """Submit a timesheet correction request from the portal."""
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        ICP = request.env['ir.config_parameter'].sudo()
        if ICP.get_param('fusion_clock.enable_correction_requests', 'True') != 'True':
            return {'error': 'Correction requests are not enabled.'}

        if not attendance_id or not reason:
            return {'error': 'Please provide the attendance record and a reason.'}

        attendance = request.env['hr.attendance'].sudo().browse(attendance_id)
        if not attendance.exists() or attendance.employee_id.id != employee.id:
            return {'error': 'Attendance record not found.'}

        vals = {
            'employee_id': employee.id,
            'attendance_id': attendance_id,
            'reason': reason,
        }
        if check_in:
            try:
                vals['requested_check_in'] = fields.Datetime.from_string(check_in)
            except Exception:
                pass
        if check_out:
            try:
                vals['requested_check_out'] = fields.Datetime.from_string(check_out)
            except Exception:
                pass

        request.env['fusion.clock.correction'].sudo().create(vals)
        return {'success': True, 'message': 'Correction request submitted for review.'}

    @http.route('/fusion_clock/get_status', type='jsonrpc', auth='user', methods=['POST'])
    def get_status(self, **kw):
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        is_checked_in = employee.attendance_state == 'checked_in'

        result = {
            'is_checked_in': is_checked_in,
            'employee_name': employee.name,
            'enable_clock': employee.x_fclk_enable_clock,
            'pending_reason': employee.x_fclk_pending_reason,
            'ontime_streak': employee.x_fclk_ontime_streak,
        }

        if is_checked_in:
            att = request.env['hr.attendance'].sudo().search([
                ('employee_id', '=', employee.id),
                ('check_out', '=', False),
            ], limit=1)
            if att:
                result.update({
                    'attendance_id': att.id,
                    'check_in': fields.Datetime.to_string(att.check_in),
                    'location_name': att.x_fclk_location_id.name or '',
                    'location_id': att.x_fclk_location_id.id or False,
                })

        today_start = fields.Datetime.to_string(
            datetime.combine(fields.Date.today(), datetime.min.time())
        )
        today_atts = request.env['hr.attendance'].sudo().search([
            ('employee_id', '=', employee.id),
            ('check_in', '>=', today_start),
            ('check_out', '!=', False),
        ])
        result['today_hours'] = round(sum(a.x_fclk_net_hours or 0 for a in today_atts), 2)

        today = fields.Date.today()
        week_start = today - timedelta(days=today.weekday())
        week_start_dt = fields.Datetime.to_string(
            datetime.combine(week_start, datetime.min.time())
        )
        week_atts = request.env['hr.attendance'].sudo().search([
            ('employee_id', '=', employee.id),
            ('check_in', '>=', week_start_dt),
            ('check_out', '!=', False),
        ])
        result['week_hours'] = round(sum(a.x_fclk_net_hours or 0 for a in week_atts), 2)

        recent = request.env['hr.attendance'].sudo().search([
            ('employee_id', '=', employee.id),
            ('check_out', '!=', False),
        ], order='check_in desc', limit=10)
        result['recent_activity'] = [{
            'id': a.id,
            'check_in': fields.Datetime.to_string(a.check_in),
            'check_out': fields.Datetime.to_string(a.check_out),
            'worked_hours': round(a.worked_hours or 0, 2),
            'net_hours': round(a.x_fclk_net_hours or 0, 2),
            'location': a.x_fclk_location_id.name or '',
        } for a in recent]

        return result

    @http.route('/fusion_clock/get_locations', type='jsonrpc', auth='user', methods=['POST'])
    def get_locations(self, **kw):
        employee = self._get_employee()
        if not employee:
            return {'error': 'No employee record found for current user.'}

        locations = self._get_locations_for_employee(employee)
        default_id = employee.x_fclk_default_location_id.id if employee.x_fclk_default_location_id else False

        return {
            'locations': [{
                'id': loc.id,
                'name': loc.name,
                'address': loc.address or '',
                'latitude': loc.latitude,
                'longitude': loc.longitude,
                'radius': loc.radius,
                'is_default': loc.id == default_id,
                'require_photo': loc.require_photo,
            } for loc in locations],
            'default_location_id': default_id,
        }

    @http.route('/fusion_clock/get_settings', type='jsonrpc', auth='user', methods=['POST'])
    def get_settings(self, **kw):
        ICP = request.env['ir.config_parameter'].sudo()
        return {
            'enable_sounds': ICP.get_param('fusion_clock.enable_sounds', 'True') == 'True',
            'google_maps_api_key': ICP.get_param('fusion_clock.google_maps_api_key', ''),
            'default_clock_in': float(ICP.get_param('fusion_clock.default_clock_in_time', '9.0')),
            'default_clock_out': float(ICP.get_param('fusion_clock.default_clock_out_time', '17.0')),
            'target_daily_hours': 8.0,
            'target_weekly_hours': 40.0,
            'enable_photo': ICP.get_param('fusion_clock.enable_photo_verification', 'False') == 'True',
            'enable_corrections': ICP.get_param('fusion_clock.enable_correction_requests', 'True') == 'True',
        }

    @http.route('/fusion_clock/dashboard_data', type='jsonrpc', auth='user', methods=['POST'])
    def dashboard_data(self, **kw):
        """Return dashboard data for managers."""
        user = request.env.user
        is_manager = user.has_group('fusion_clock.group_fusion_clock_manager')
        is_team_lead = user.has_group('fusion_clock.group_fusion_clock_team_lead')

        if not is_manager and not is_team_lead:
            return {'error': 'Access denied.'}

        now = fields.Datetime.now()
        today = fields.Date.today()
        today_start = datetime.combine(today, datetime.min.time())

        Attendance = request.env['hr.attendance'].sudo()
        Employee = request.env['hr.employee'].sudo()

        # Filter employees by access
        if is_manager:
            employees = Employee.search([('x_fclk_enable_clock', '=', True)])
        else:
            employee = self._get_employee()
            if not employee:
                return {'error': 'No employee record found.'}
            employees = Employee.search([
                ('parent_id', '=', employee.id),
                ('x_fclk_enable_clock', '=', True),
            ])

        emp_ids = employees.ids

        # Currently clocked in
        open_atts = Attendance.search([
            ('employee_id', 'in', emp_ids),
            ('check_out', '=', False),
        ])
        clocked_in = [{
            'employee': a.employee_id.name,
            'check_in': fields.Datetime.to_string(a.check_in),
            'location': a.x_fclk_location_id.name or '',
        } for a in open_atts]

        # Today stats
        today_atts = Attendance.search([
            ('employee_id', 'in', emp_ids),
            ('check_in', '>=', today_start),
        ])
        present_ids = set(a.employee_id.id for a in today_atts)

        ActivityLog = request.env['fusion.clock.activity.log'].sudo()
        late_count = ActivityLog.search_count([
            ('employee_id', 'in', emp_ids),
            ('log_type', '=', 'late_clock_in'),
            ('log_date', '>=', today_start),
        ])

        # Pending alerts
        pending_reasons = Employee.search_count([
            ('id', 'in', emp_ids),
            ('x_fclk_pending_reason', '=', True),
        ])
        pending_corrections = request.env['fusion.clock.correction'].sudo().search_count([
            ('employee_id', 'in', emp_ids),
            ('state', '=', 'pending'),
        ])

        return {
            'clocked_in': clocked_in,
            'total_employees': len(emp_ids),
            'present_count': len(present_ids),
            'absent_count': len(emp_ids) - len(present_ids),
            'late_count': late_count,
            'pending_reasons': pending_reasons,
            'pending_corrections': pending_corrections,
        }
