from datetime import datetime, timedelta
from zoneinfo import ZoneInfo


def is_salon_holiday(day: datetime, salon: dict, timezone: str) -> bool:
    tz = ZoneInfo(timezone)
    for holiday in salon.get("holidays") or []:
        holiday_date = holiday.get("date")
        if not holiday_date:
            continue
        if isinstance(holiday_date, datetime):
            h = holiday_date.astimezone(tz) if holiday_date.tzinfo else holiday_date.replace(tzinfo=tz)
        else:
            h = datetime.fromisoformat(str(holiday_date).replace("Z", "+00:00")).astimezone(tz)
        if h.date() == day.date():
            return True
    return False


def get_salon_hours(day: datetime, salon: dict) -> dict | None:
    weekday = day.strftime("%A")
    for entry in salon.get("opening_hours") or []:
        if entry.get("day") == weekday and not entry.get("closed"):
            return entry
    return None


def get_stylist_hours(day: datetime, stylist: dict) -> dict | None:
    weekday = day.strftime("%A").lower()
    for entry in stylist.get("workingHours") or []:
        if entry.get("day") == weekday and entry.get("enabled"):
            return entry
    return None


def is_stylist_on_vacation(day: datetime, stylist: dict, timezone: str) -> bool:
    tz = ZoneInfo(timezone)
    for vacation in stylist.get("vacations") or []:
        start_raw = vacation.get("start")
        end_raw = vacation.get("end")
        if not start_raw or not end_raw:
            continue
        start = _to_local_datetime(start_raw, tz).replace(
            hour=0, minute=0, second=0, microsecond=0
        )
        end = _to_local_datetime(end_raw, tz).replace(
            hour=23, minute=59, second=59, microsecond=999999
        )
        if start <= day <= end:
            return True
    return False


def get_working_window(
    day: datetime, salon_hours: dict, stylist_hours: dict, timezone: str
) -> tuple[datetime, datetime] | None:
    tz = ZoneInfo(timezone)
    day_str = day.strftime("%Y-%m-%d")
    start = datetime.fromisoformat(f"{day_str}T{salon_hours['open']}").replace(tzinfo=tz)
    end = datetime.fromisoformat(f"{day_str}T{salon_hours['close']}").replace(tzinfo=tz)

    for slot in stylist_hours.get("slots") or []:
        slot_start = datetime.fromisoformat(f"{day_str}T{slot['start']}").replace(tzinfo=tz)
        slot_end = datetime.fromisoformat(f"{day_str}T{slot['end']}").replace(tzinfo=tz)
        start = max(start, slot_start)
        end = min(end, slot_end)

    return (start, end) if start < end else None


def is_same_salon_day(appointment_start, day: datetime, timezone: str) -> bool:
    tz = ZoneInfo(timezone)
    start = _to_local_datetime(appointment_start, tz)
    return start.date() == day.date()


def subtract_appointments(
    window_start: datetime,
    window_end: datetime,
    appointments: list[dict],
    timezone: str,
) -> list[dict[str, str]]:
    tz = ZoneInfo(timezone)
    blocks = sorted(
        [
            {
                "start": _to_local_datetime(appt["start"], tz),
                "end": _to_local_datetime(appt["end"], tz),
            }
            for appt in appointments
        ],
        key=lambda block: block["start"],
    )

    free: list[dict[str, str]] = []
    cursor = window_start

    for block in blocks:
        if cursor < block["start"]:
            free.append(
                {
                    "start": cursor.isoformat(),
                    "end": block["start"].isoformat(),
                }
            )
        cursor = max(cursor, block["end"])

    if cursor < window_end:
        free.append({"start": cursor.isoformat(), "end": window_end.isoformat()})

    return free


def build_stylist_availability(
    stylist: dict,
    salon: dict,
    appointments: list[dict],
    duration: int,
    start_day: datetime,
    timezone: str,
) -> list[dict]:
    days: list[dict] = []
    duration = duration or 0

    for offset in range(7):
        day = start_day + timedelta(days=offset)

        if is_salon_holiday(day, salon, timezone):
            continue

        salon_hours = get_salon_hours(day, salon)
        if not salon_hours:
            continue

        stylist_hours = get_stylist_hours(day, stylist)
        if not stylist_hours:
            continue

        if is_stylist_on_vacation(day, stylist, timezone):
            continue

        window = get_working_window(day, salon_hours, stylist_hours, timezone)
        if not window:
            continue

        window_start, window_end = window
        stylist_id = str(stylist["_id"])
        day_appointments = [
            appt
            for appt in appointments
            if str(appt.get("stylistId")) == stylist_id
            and is_same_salon_day(appt.get("start"), day, timezone)
        ]

        slots = subtract_appointments(
            window_start, window_end, day_appointments, timezone
        )

        valid_slots = []
        tz = ZoneInfo(timezone)
        for slot in slots:
            slot_start = datetime.fromisoformat(slot["start"]).astimezone(tz)
            slot_end = datetime.fromisoformat(slot["end"]).astimezone(tz)
            if slot_end - slot_start >= timedelta(minutes=duration):
                valid_slots.append(slot)

        if valid_slots:
            days.append({"date": day.strftime("%Y-%m-%d"), "slots": valid_slots})

    return days


def _to_local_datetime(value, tz: ZoneInfo) -> datetime:
    if isinstance(value, datetime):
        dt = value
    else:
        dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
    if dt.tzinfo is None:
        return dt.replace(tzinfo=tz)
    return dt.astimezone(tz)
