from datetime import datetime, time, timedelta

import pytz

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


class McsAppointmentType(models.Model):
    _name = "mcs.appointment.type"
    _description = "Appointment Type"
    _inherit = ["mail.thread", "mail.activity.mixin"]
    _order = "sequence, name"

    name = fields.Char(required=True, tracking=True)
    sequence = fields.Integer(default=10)
    active = fields.Boolean(default=True)
    website_published = fields.Boolean(string="Published on Website", default=True)
    company_id = fields.Many2one(
        "res.company", default=lambda self: self.env.company, required=True
    )
    user_id = fields.Many2one(
        "res.users",
        string="Host",
        default=lambda self: self.env.user,
        required=True,
        tracking=True,
    )
    calendar_id = fields.Many2one(
        "resource.calendar",
        string="Availability Calendar",
        required=True,
        default=lambda self: self.env.company.resource_calendar_id,
    )
    duration = fields.Float(
        string="Duration",
        default=1.0,
        required=True,
        help="Appointment duration in hours.",
    )
    slot_step = fields.Float(
        string="Slot Step",
        default=0.5,
        required=True,
        help="Distance between proposed slots in hours.",
    )
    min_schedule_hours = fields.Float(
        string="Minimum Notice",
        default=2.0,
        help="Minimum hours before a visitor can book.",
    )
    max_schedule_days = fields.Integer(
        string="Book Up To",
        default=30,
        help="Maximum number of future days shown for booking.",
    )
    description = fields.Html()
    booking_ids = fields.One2many("mcs.appointment.booking", "appointment_type_id")
    booking_count = fields.Integer(compute="_compute_booking_count")

    @api.depends("booking_ids")
    def _compute_booking_count(self):
        grouped = self.env["mcs.appointment.booking"].read_group(
            [("appointment_type_id", "in", self.ids)],
            ["appointment_type_id"],
            ["appointment_type_id"],
        )
        counts = {row["appointment_type_id"][0]: row["appointment_type_id_count"] for row in grouped}
        for record in self:
            record.booking_count = counts.get(record.id, 0)

    @api.constrains("duration", "slot_step", "max_schedule_days")
    def _check_positive_values(self):
        for record in self:
            if record.duration <= 0:
                raise ValidationError("Appointment duration must be positive.")
            if record.slot_step <= 0:
                raise ValidationError("Slot step must be positive.")
            if record.max_schedule_days <= 0:
                raise ValidationError("Book up to must be positive.")

    def action_view_bookings(self):
        self.ensure_one()
        return {
            "name": "Bookings",
            "type": "ir.actions.act_window",
            "res_model": "mcs.appointment.booking",
            "view_mode": "tree,form,calendar",
            "domain": [("appointment_type_id", "=", self.id)],
            "context": {"default_appointment_type_id": self.id},
        }

    def _public_domain(self):
        return [("active", "=", True), ("website_published", "=", True)]

    def _timezone(self):
        return pytz.timezone(self.calendar_id.tz or self.env.user.tz or "UTC")

    def _week_type_for_date(self, day):
        return "1" if day.isocalendar()[1] % 2 == 0 else "0"

    def _float_to_time(self, value):
        hours = int(value)
        minutes = int(round((value - hours) * 60))
        if minutes == 60:
            hours += 1
            minutes = 0
        return time(hour=hours, minute=minutes)

    def _localize(self, day, hour_float):
        tz = self._timezone()
        local_dt = datetime.combine(day, self._float_to_time(hour_float))
        return tz.localize(local_dt).astimezone(pytz.UTC).replace(tzinfo=None)

    def _busy_intervals(self, day_start, day_end):
        events = self.env["calendar.event"].sudo().search(
            [
                ("user_id", "=", self.user_id.id),
                ("start", "<", fields.Datetime.to_string(day_end)),
                ("stop", ">", fields.Datetime.to_string(day_start)),
            ]
        )
        return [(event.start, event.stop) for event in events if event.start and event.stop]

    def _is_free(self, start, stop, busy_intervals):
        for busy_start, busy_stop in busy_intervals:
            if start < busy_stop and stop > busy_start:
                return False
        return True

    def get_available_slots(self, day, limit=40):
        self.ensure_one()
        day = fields.Date.to_date(day)
        now = fields.Datetime.now() + timedelta(hours=self.min_schedule_hours)
        if day < fields.Date.context_today(self):
            return []

        week_type = self._week_type_for_date(day)
        attendances = self.calendar_id.attendance_ids.filtered(
            lambda line: line.dayofweek == str(day.weekday())
            and line.day_period != "lunch"
            and (not self.calendar_id.two_weeks_calendar or line.week_type == week_type)
        )
        day_start = self._localize(day, 0)
        day_end = self._localize(day, 23.99)
        busy_intervals = self._busy_intervals(day_start, day_end)
        slots = []
        step = timedelta(hours=self.slot_step)
        duration = timedelta(hours=self.duration)
        for attendance in attendances.sorted("hour_from"):
            cursor = self._localize(day, attendance.hour_from)
            end_limit = self._localize(day, attendance.hour_to)
            while cursor + duration <= end_limit:
                stop = cursor + duration
                if cursor >= now and self._is_free(cursor, stop, busy_intervals):
                    slots.append({"start": cursor, "stop": stop})
                    if len(slots) >= limit:
                        return slots
                cursor += step
        return slots
