"""Pure recurrence arithmetic; local date intent is kept across clock changes."""
import calendar
from fastapi import HTTPException
from datetime import date,datetime,time,timedelta
from .scheduling import BookingFields,prepare

WEEKS={'weekly':1,'fortnightly':2};MONTHS={'monthly':1,'quarterly':3,'yearly':12}

def applicable(rules,day):
    return max((r for r in rules if r['effective_from']<=day),key=lambda r:r['revision'],default=None)

def scheduled(rule,day):
    value=rule['content'];d=date.fromisoformat(day);anchor=date.fromisoformat(value['anchor_date'])
    if day<value['anchor_date'] or day>value['until_date']:return False
    if value['frequency'] in WEEKS:
        week0=anchor-timedelta(days=anchor.weekday());week=d-timedelta(days=d.weekday())
        return ((week-week0).days//7)%WEEKS[value['frequency']]==0 and d.weekday() in value['weekdays']
    delta=(d.year-anchor.year)*12+d.month-anchor.month
    if delta%MONTHS[value['frequency']]:return False
    last=calendar.monthrange(d.year,d.month)[1];wanted=value['month_day']
    if wanted>last and value['month_policy']=='skip':return False
    return d.day==min(wanted,last)

def invalid_months(rules,start,end):
    """Visible skipped months; an impossible date is never used as an occurrence ID."""
    rows=[];day=date.fromisoformat(start).replace(day=1);stop=date.fromisoformat(end)
    while day<=stop:
        last=calendar.monthrange(day.year,day.month)[1];key=day.replace(day=last).isoformat();rule=applicable(rules,key)
        if rule:
            value=rule['content'];anchor=date.fromisoformat(value['anchor_date']);delta=(day.year-anchor.year)*12+day.month-anchor.month
            if value['frequency'] in MONTHS and value['month_policy']=='skip' and delta>=0 and delta%MONTHS[value['frequency']]==0 and value['month_day']>last and value['anchor_date']<=key and value['until_date']>=day.isoformat() and key>=start:
                rows.append({'month':day.strftime('%Y-%m'),'reason':f"No day {value['month_day']} in this month; explicit skip policy."})
        day=(day.replace(day=28)+timedelta(days=4)).replace(day=1)
    return rows

def candidate(db,contract,rule,day):
    value=rule['content'];d=date.fromisoformat(day)
    if value['all_day']:
        end=d+timedelta(days=value['duration_days']-1);start_time=end_time=''
    else:
        finish=datetime.combine(d,time.fromisoformat(value['start_time']))+timedelta(minutes=value['duration_minutes']);end=finish.date();start_time=value['start_time'];end_time=finish.strftime('%H:%M')
    if end.year>2100:raise HTTPException(422,'The visit would extend beyond the supported year 2100.')
    payload=BookingFields(kind=contract['kind'],job_id=contract['job_id'],title=contract['name'],resource_ids=value['resource_ids'],whole_team=value['whole_team'],all_day=value['all_day'],start_date=day,end_date=end.isoformat(),start_time=start_time,end_time=end_time,start_fold=value['start_fold'],end_fold=value['end_fold'],note=contract['note'])
    return prepare(db,payload)

def covered_end(booking):
    end=date.fromisoformat(booking['end_date'])
    if not booking['all_day'] and booking['end_time']=='00:00':end-=timedelta(days=1)
    return end.isoformat()

def exclusions(blocks,nominal,booking=None):
    return [b['id'] for b in blocks if b['active'] and (b['start_date']<=nominal<=b['end_date'] or booking is not None and b['start_date']<=covered_end(booking) and b['end_date']>=booking['start_date'])]
