"""Bounded owner-reviewed recurrence contracts. No automatic work or billing."""
from datetime import date,time
from typing import Annotated,Literal,Union
from pydantic import Field,StrictBool,StrictInt,field_validator,model_validator
from .models import Strict
from .material_rules import Identity,VersionNumber
from .scheduling import BookingFields,calendar_date

SCHEMA='''
CREATE TABLE IF NOT EXISTS recurring_contracts(id TEXT PRIMARY KEY,kind TEXT NOT NULL,job_id TEXT REFERENCES jobs(id),customer_id TEXT REFERENCES customers(id),name TEXT NOT NULL,service_label TEXT NOT NULL,note TEXT NOT NULL,state TEXT NOT NULL,version INTEGER NOT NULL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS recurrence_rules(id TEXT PRIMARY KEY,contract_id TEXT NOT NULL REFERENCES recurring_contracts(id),revision INTEGER NOT NULL,effective_from TEXT NOT NULL,content TEXT NOT NULL,author_id TEXT NOT NULL,membership_id TEXT NOT NULL,created_at TEXT NOT NULL,UNIQUE(contract_id,revision));
CREATE TABLE IF NOT EXISTS recurrence_blocks(id TEXT PRIMARY KEY,contract_id TEXT NOT NULL REFERENCES recurring_contracts(id),start_date TEXT NOT NULL,end_date TEXT NOT NULL,reason TEXT NOT NULL,active INTEGER NOT NULL,version INTEGER NOT NULL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS recurrence_occurrences(id TEXT PRIMARY KEY,contract_id TEXT NOT NULL REFERENCES recurring_contracts(id),nominal_date TEXT NOT NULL,original_rule_id TEXT NOT NULL REFERENCES recurrence_rules(id),rule_id TEXT NOT NULL REFERENCES recurrence_rules(id),rule_snapshot TEXT NOT NULL,booking_id TEXT UNIQUE REFERENCES calendar_bookings(id),state TEXT NOT NULL,overridden INTEGER NOT NULL,exclusion_ids TEXT NOT NULL,reason TEXT NOT NULL,version INTEGER NOT NULL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL,UNIQUE(contract_id,nominal_date));
CREATE TABLE IF NOT EXISTS recurrence_history(id INTEGER PRIMARY KEY AUTOINCREMENT,contract_id TEXT NOT NULL REFERENCES recurring_contracts(id),kind TEXT NOT NULL,record_id TEXT NOT NULL,action TEXT NOT NULL,actor_id TEXT NOT NULL,membership_id TEXT NOT NULL,before_value TEXT,after_value TEXT NOT NULL,detail TEXT NOT NULL,reason TEXT NOT NULL,created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS recurrence_runs(id TEXT PRIMARY KEY,contract_id TEXT NOT NULL REFERENCES recurring_contracts(id),actor_id TEXT NOT NULL,membership_id TEXT NOT NULL,preview_hash TEXT NOT NULL,operation TEXT NOT NULL,plan TEXT NOT NULL,result TEXT NOT NULL,acknowledge_retained INTEGER NOT NULL,created_at TEXT NOT NULL);
'''

class Dates(Strict):
    @field_validator('anchor_date','until_date','effective_from','start_date','end_date','window_end','nominal_date',check_fields=False)
    @classmethod
    def day(cls,value):return calendar_date(value).isoformat()

class Rule(Dates):
    frequency:Literal['weekly','fortnightly','monthly','quarterly','yearly']='weekly'
    anchor_date:str
    until_date:str
    weekdays:list[Annotated[StrictInt,Field(ge=0,le=6)]]=Field(default_factory=list,max_length=7)
    month_day:StrictInt|None=Field(default=None,ge=1,le=31)
    month_policy:Literal['skip','last_day']|None=None
    resource_ids:list[Identity]=Field(default_factory=list,max_length=50)
    whole_team:StrictBool=False
    all_day:StrictBool=False
    start_time:str=Field(default='09:00',pattern=r'^(?:[01]\d|2[0-3]):[0-5]\d$')
    duration_minutes:StrictInt|None=Field(default=60,ge=1,le=1440)
    duration_days:StrictInt|None=Field(default=None,ge=1,le=7)
    start_fold:StrictInt|None=Field(default=None,ge=0,le=1)
    end_fold:StrictInt|None=Field(default=None,ge=0,le=1)
    gap_policy:Literal['require_review','skip']='require_review'
    rolling_weeks:Literal[2,4,6,8,12]=4
    @model_validator(mode='after')
    def choices(self):
        if not 0<=(date.fromisoformat(self.until_date)-date.fromisoformat(self.anchor_date)).days<=5*366:raise ValueError('Choose a finite rule horizon of at most five years.')
        if self.frequency in ('weekly','fortnightly'):
            if not self.weekdays or len(set(self.weekdays))!=len(self.weekdays) or self.month_day is not None or self.month_policy is not None:raise ValueError('Weekly rules need distinct weekdays and no month-day choices.')
        elif self.weekdays or self.month_day is None or self.month_policy is None:raise ValueError('Monthly rules need an explicit day and missing-day policy, with no weekdays.')
        if len(set(self.resource_ids))!=len(self.resource_ids) or self.whole_team==bool(self.resource_ids):raise ValueError('Choose named resources or whole-team unavailability.')
        if self.all_day:
            if self.duration_days is None or self.duration_minutes is not None or self.start_fold is not None or self.end_fold is not None:raise ValueError('All-day rules use duration days, with no timed duration or clock choices.')
        elif self.duration_minutes is None or self.duration_days is not None:raise ValueError('Timed rules use wall-clock minutes, with no duration days.')
        return self

class Metadata(Strict):
    name:str=Field(min_length=2,max_length=160)
    service_label:str=Field(default='',max_length=80)
    note:str=Field(default='',max_length=1000)

class Create(Metadata,Dates):
    kind:Literal['job','unavailable']='job'
    job_id:Identity|None=None
    customer_id:Identity|None=None
    job_version:VersionNumber|None=None
    effective_from:str
    rule:Rule
    @model_validator(mode='after')
    def binding(self):
        if self.kind=='job':
            if self.job_id is None or self.customer_id is None or self.job_version is None or self.rule.whole_team:raise ValueError('Work contracts need an exact job/customer/version and named people.')
        elif any(x is not None for x in (self.job_id,self.customer_id,self.job_version)):raise ValueError('Unavailable contracts have no customer or job.')
        return self

class Window(Dates):
    start_date:str
    end_date:str
    @model_validator(mode='after')
    def bound(self):
        if not 0<=(date.fromisoformat(self.end_date)-date.fromisoformat(self.start_date)).days<=365:raise ValueError('Choose an inclusive window of at most 366 dates.')
        return self

class Generate(Window):
    action:Literal['generate']='generate'

class Revise(Metadata,Dates):
    action:Literal['revise']='revise'
    effective_from:str
    window_end:str
    rule:Rule
    reason:str=Field(min_length=3,max_length=500)
    @model_validator(mode='after')
    def window(self):Window(start_date=self.effective_from,end_date=self.window_end);return self

class State(Strict):
    action:Literal['state']='state'
    state:Literal['active','paused','retired','restore']
    reason:str=Field(min_length=3,max_length=500)

class Block(Window):
    action:Literal['block']='block'
    reason:str=Field(min_length=3,max_length=500)

class Unblock(Strict):
    action:Literal['unblock']='unblock'
    block_id:Identity
    block_version:VersionNumber
    reason:str=Field(min_length=3,max_length=500)

class Occurrence(Dates):
    nominal_date:str
    occurrence_version:StrictInt=Field(default=0,ge=0)
    booking_version:VersionNumber|None=None
    acknowledge_history:StrictBool=False
    reason:str=Field(min_length=3,max_length=500)

class Override(Occurrence):
    action:Literal['override']='override'
    booking:BookingFields

class Cancel(Occurrence):action:Literal['cancel']='cancel'
class Exclude(Occurrence):action:Literal['exclude']='exclude'
class Restore(Occurrence):action:Literal['restore']='restore'
Operation=Annotated[Union[Generate,Revise,State,Block,Unblock,Override,Cancel,Exclude,Restore],Field(discriminator='action')]
class Preview(Strict):
    version:VersionNumber
    operation:Operation
class Confirm(Preview):
    preview_hash:str=Field(pattern='^[a-f0-9]{64}$')
    confirmed:StrictBool
    acknowledge_retained:StrictBool=False
    @field_validator('confirmed')
    @classmethod
    def reviewed(cls,value):
        if not value:raise ValueError('Review the whole preview before confirming.')
        return value
