# backend/app/routers/calendar.py
import re
import uuid
from datetime import date

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select

from ..connectors.calendar import get_calendar_connector, real_google, real_outlook
from ..connectors.calendar.normalize import account_calendar_id
from ..db import get_session
from ..models import (
    Calendar,
    CalEvent,
    ConnectorAccount,
    ConnectorDomain,
    ConnectorMode,
    ConnState,
    ExternalLink,
    FocusBlock,
    Meeting,
    MeetingAction,
    Task,
)
from ..schemas import (
    AttendeeOut,
    CalendarOut,
    CalEventOut,
    DayBundleOut,
    EventActionOut,
    EventRsvpRequest,
    EventWriteRequest,
    FocusBlockOut,
    FocusSuggestRequest,
    FocusSuggestResponse,
    MaterializeResponse,
    MeetingActionOut,
    MeetingOut,
    WeekBundleOut,
)
from ..services.focus import suggest_focus
from ..services.meetings import assemble_meeting, materialize_action, materialize_all
from .tasks import to_node  # phase-2 TaskNode 빌더 재사용

router = APIRouter()  # prefix 없음. main.py 에서 prefix="/api" 등록.

WEEK = [7, 8, 9, 10, 11, 12, 13]
WEEKDAYS = ["일", "월", "화", "수", "목", "금", "토"]
TODAY = 8
DEMO_TODAY_DATE = "2026-06-08"  # 데모(시드) 기준일 — 실연결 없을 때


def _display_date(day: int, real_date: str) -> str:
    """이벤트 표시용 ISO 날짜. 실데이터는 date, 데모(시드)는 6월 day 로 매핑."""
    if real_date:
        return real_date
    try:
        return f"2026-06-{int(day):02d}"
    except (TypeError, ValueError):
        return DEMO_TODAY_DATE


def _calendar_mode(s: Session) -> tuple[str, str]:
    """(mode, today_date). real 캘린더가 연결돼 있으면 실제 오늘, 아니면 데모 기준일."""
    has_real = s.exec(
        select(ConnectorAccount).where(
            ConnectorAccount.domain == ConnectorDomain.calendar,
            ConnectorAccount.mode == ConnectorMode.real,
            ConnectorAccount.state == ConnState.connected,
        )
    ).first()
    return ("real", date.today().isoformat()) if has_real else ("demo", DEMO_TODAY_DATE)


def _end_from(start: str, dur: int) -> str:
    h, m = map(int, start.split(":"))
    total = h * 60 + m + dur
    return f"{total // 60:02d}:{total % 60:02d}"


def meet_label(m: Meeting | None) -> str:
    if m is None:
        return ""
    phase = getattr(m.phase, "value", m.phase)
    if m.one_on_one:
        return "1:1 어시스턴트 브리핑"
    if phase == "upcoming":
        return "사전 브리핑 보기"
    if phase == "live":
        return "실시간 기록 보기"
    return "회의 노트 · 액션 보기"


def _parse_day(ev: dict) -> int:
    """ev 에서 6월 날짜(int) 추출. 'date'가 'M/D'면 일(D), 'day'가 숫자면 그 값, 아니면 TODAY."""
    date_str = str(ev.get("date") or "")
    if "/" in date_str:
        try:
            return int(date_str.split("/")[1])
        except (ValueError, IndexError):
            pass
    try:
        return int(ev.get("day"))
    except (TypeError, ValueError):
        return TODAY


def create_event_from_extract(s: Session, ev: dict, source: str) -> str:
    """메일→일정 federation 헬퍼. ev = {title, date|day, time, dur, place?} →
    CalEvent 생성(연합 출처 source 기록) 후 id 반환. phase-9 routers/mail.py 가 import.
    메일 AI 이벤트(date='6/10', day='오늘', time='14:00'|'종일')도 견고하게 처리."""
    eid = "ce-" + uuid.uuid4().hex[:8]
    day = _parse_day(ev)
    m = re.match(r"(\d{1,2}):(\d{2})", str(ev.get("time") or ""))
    if m:
        start = f"{int(m.group(1)):02d}:{m.group(2)}"
        dur_digits = re.sub(r"\D", "", str(ev.get("dur") or "")) or "60"
        end = _end_from(start, int(dur_digits))
    else:  # 종일/미정 시간
        start, end = "09:00", "09:30"
    conn = get_calendar_connector()
    conn.write_event(
        s,
        dict(
            id=eid,
            day=day,
            start=start,
            end=end,
            title=ev["title"],
            cal="meeting",
            loc=ev.get("place", ""),
            note=f"연합 출처: {source}",
            soon=False,
            people="",
            sort_order=0,
        ),
    )
    return eid


def _event_out(s: Session, e: CalEvent) -> CalEventOut:
    m = s.get(Meeting, e.id)
    eacts = s.exec(
        select(MeetingAction)
        .where(MeetingAction.event_id == e.id, MeetingAction.source == "event")
        .order_by(MeetingAction.idx)
    ).all()
    return CalEventOut(
        id=e.id,
        day=e.day,
        date=_display_date(e.day, e.date),
        start=e.start,
        end=e.end,
        title=e.title,
        cal=e.cal,
        loc=e.loc,
        note=e.note,
        soon=e.soon,
        people=[p.strip() for p in e.people.split(",") if p.strip()],
        rrule=e.rrule or "",
        reminders=list(e.reminders or []),
        attendees=[
            AttendeeOut(
                email=a.get("email", ""), name=a.get("name", ""), status=a.get("status", "")
            )
            for a in (e.attendees or [])
        ],
        response_status=e.response_status or "",
        actions=[EventActionOut(text=a.text, when=a.when_text) for a in eacts],
        has_meeting=m is not None,
        meet_label=meet_label(m),
    )


@router.get("/calendar/week", response_model=WeekBundleOut)
def get_week(s: Session = Depends(get_session)):
    conn = get_calendar_connector()
    rows = [s.get(CalEvent, r["id"]) for r in conn.list_events(s)]
    cals = s.exec(select(Calendar).order_by(Calendar.sort_order)).all()
    counts: dict[str, int] = {}
    for e in rows:
        counts[e.cal] = counts.get(e.cal, 0) + 1
    fbs = s.exec(select(FocusBlock).order_by(FocusBlock.sort_order)).all()
    mode, today_date = _calendar_mode(s)
    return WeekBundleOut(
        today=TODAY,
        today_date=today_date,
        mode=mode,
        week=WEEK,
        weekdays=WEEKDAYS,
        calendars=[
            CalendarOut(id=c.id, name=c.name, tone=c.tone, on=c.on, count=counts.get(c.id, 0))
            for c in cals
        ],
        events=[_event_out(s, e) for e in rows],
        focus_blocks=[_focus_out(f) for f in fbs],
    )


def _focus_out(f: FocusBlock) -> FocusBlockOut:
    d = f.model_dump()
    d["date"] = _display_date(f.day, "")  # 집중 블록은 데모 전용 → 6월로 매핑
    return FocusBlockOut(**d)


@router.get("/calendar/day/{day}", response_model=DayBundleOut)
def get_day(day: int, s: Session = Depends(get_session)):
    evs = s.exec(select(CalEvent).where(CalEvent.day == day).order_by(CalEvent.start)).all()
    fbs = s.exec(select(FocusBlock).where(FocusBlock.day == day).order_by(FocusBlock.start)).all()
    return DayBundleOut(
        day=day,
        events=[_event_out(s, e) for e in evs],
        focus_blocks=[_focus_out(f) for f in fbs],
    )


@router.get("/calendar/meetings/{mid}", response_model=MeetingOut)
def get_meeting(mid: str, s: Session = Depends(get_session)):
    data = assemble_meeting(s, mid)
    if data is None:
        raise HTTPException(404, "meeting not found")
    return MeetingOut(**data)


@router.post(
    "/calendar/meetings/{mid}/actions/{idx}/materialize", response_model=MaterializeResponse
)
def materialize_one(mid: str, idx: int, s: Session = Depends(get_session)):
    try:
        action, task = materialize_action(s, mid, idx)
    except ValueError as e:
        raise HTTPException(404, "action not found") from e
    remaining = s.exec(
        select(MeetingAction).where(
            MeetingAction.meeting_id == mid, MeetingAction.added == False  # noqa: E712
        )
    ).all()
    all_tasks = s.exec(select(Task)).all()
    return MaterializeResponse(
        action=MeetingActionOut(
            id=action.id,
            idx=action.idx,
            text=action.text,
            who=action.who,
            when=action.when_text,
            added=action.added,
            materialized_task_id=action.materialized_task_id,
        ),
        task=to_node(s, task, all_tasks),
        all_added=len(remaining) == 0,
    )


@router.post(
    "/calendar/meetings/{mid}/actions/materialize-all",
    response_model=list[MaterializeResponse],
)
def materialize_remaining(mid: str, s: Session = Depends(get_session)):
    pairs = materialize_all(s, mid)
    all_tasks = s.exec(select(Task)).all()
    return [
        MaterializeResponse(
            action=MeetingActionOut(
                id=a.id,
                idx=a.idx,
                text=a.text,
                who=a.who,
                when=a.when_text,
                added=a.added,
                materialized_task_id=a.materialized_task_id,
            ),
            task=to_node(s, t, all_tasks),
            all_added=True,
        )
        for a, t in pairs
    ]


@router.post("/calendar/focus/suggest", response_model=FocusSuggestResponse)
def focus_suggest(body: FocusSuggestRequest, s: Session = Depends(get_session)):
    blocks = suggest_focus(s, body.day, body.min_minutes)
    # commit 전에 스냅샷(커밋 후 객체 expire 방지)
    out = [FocusBlockOut(**b.model_dump()) for b in blocks]
    if body.create:
        for b in blocks:
            s.add(b)
        s.commit()
    return FocusSuggestResponse(day=body.day, created=body.create, suggestions=out)


# ── 일정 쓰기(생성·수정·삭제). 연결된 Google 캘린더에 실제 반영 + 로컬 미러. ──
LOCAL_CAL_ID = "local"


def _ensure_local_calendar(s: Session) -> str:
    """외부 캘린더 미연결 시 사용할 로컬 캘린더 행 보장."""
    if not s.get(Calendar, LOCAL_CAL_ID):
        s.add(Calendar(id=LOCAL_CAL_ID, name="내 캘린더", tone="green", on=True, sort_order=10))
        s.commit()
    return LOCAL_CAL_ID


_CAL_WRITERS = {"google_calendar": real_google, "outlook_calendar": real_outlook}


def _cal_writer(account: ConnectorAccount):
    """계정 provider 에 맞는 쓰기 모듈(real_google / real_outlook)."""
    return _CAL_WRITERS.get(account.provider)


def _writable_calendar_account(s: Session, cal_id: str = "") -> ConnectorAccount | None:
    """쓰기 가능한 연결 캘린더 계정(Google·Outlook 모두 쓰기 지원).
    cal_id 가 특정 캘린더를 가리키면 그 계정, 아니면 첫 연결 캘린더 계정."""
    accts = s.exec(
        select(ConnectorAccount).where(
            ConnectorAccount.domain == ConnectorDomain.calendar,
            ConnectorAccount.state == ConnState.connected,
        )
    ).all()
    accts = [a for a in accts if _cal_writer(a)]  # 쓰기 지원 provider 만
    if cal_id:
        for a in accts:
            if account_calendar_id(a) == cal_id:
                return a
    return accts[0] if accts else None


def _event_link(s: Session, eid: str) -> ExternalLink | None:
    return s.exec(
        select(ExternalLink).where(
            ExternalLink.entity_id == eid, ExternalLink.entity_type == "event"
        )
    ).first()


def _account_for_event(s: Session, eid: str) -> ConnectorAccount | None:
    link = _event_link(s, eid)
    return s.get(ConnectorAccount, link.account_id) if link else None


def _local_day(date_iso: str) -> int:
    try:
        return int(date_iso[8:10])
    except (ValueError, IndexError):
        return TODAY


@router.post("/calendar/events", response_model=CalEventOut)
def create_event(body: EventWriteRequest, s: Session = Depends(get_session)):
    acct = _writable_calendar_account(s, body.cal)
    if acct:  # 연결된 캘린더(Google/Outlook)에 실제 생성 후 로컬 미러
        writer = _cal_writer(acct)
        ev_json = writer.create_remote_event(s, acct, body.model_dump())
        eid = writer.mirror_event(s, acct, ev_json)
        s.commit()
    else:  # 외부 캘린더 미연결 → 로컬 전용 이벤트
        cal_id = _ensure_local_calendar(s)
        eid = "ce-" + uuid.uuid4().hex[:8]
        end = body.end or (_end_from(body.start, 60) if body.start else "")
        s.add(
            CalEvent(
                id=eid,
                day=_local_day(body.date),
                date=body.date,
                start=body.start,  # 빈값 = 종일
                end=end,
                title=body.title,
                cal=cal_id,
                loc=body.loc,
                note=body.note,
                people=", ".join(body.people),
                rrule=body.rrule,
                reminders=list(body.reminders),
                attendees=[{"email": e, "name": "", "status": "needsAction"} for e in body.people],
                soon=False,
                sort_order=0,
            )
        )
        s.commit()
    return _event_out(s, s.get(CalEvent, eid))


@router.patch("/calendar/events/{eid}", response_model=CalEventOut)
def update_event(eid: str, body: EventWriteRequest, s: Session = Depends(get_session)):
    e = s.get(CalEvent, eid)
    if not e:
        raise HTTPException(404, "event not found")
    acct = _account_for_event(s, eid)
    writer = _cal_writer(acct) if acct else None
    if acct and writer:  # 연결된 캘린더(Google/Outlook)에 실제 수정
        link = _event_link(s, eid)
        ev_json = writer.update_remote_event(s, acct, link.external_id, body.model_dump())
        writer.mirror_event(s, acct, ev_json)
        s.commit()
    else:  # 로컬 전용 이벤트
        e.title = body.title
        e.date = body.date
        e.day = _local_day(body.date)
        e.start = body.start
        e.end = body.end if body.start else ""
        e.loc = body.loc
        e.note = body.note
        e.people = ", ".join(body.people)
        e.rrule = body.rrule
        e.reminders = list(body.reminders)
        e.attendees = [{"email": p, "name": "", "status": "needsAction"} for p in body.people]
        s.add(e)
        s.commit()
    return _event_out(s, s.get(CalEvent, eid))


@router.post("/calendar/events/{eid}/rsvp", response_model=CalEventOut)
def rsvp_event(eid: str, body: EventRsvpRequest, s: Session = Depends(get_session)):
    """초대 응답(RSVP). 연결된 캘린더면 Google/Outlook 에 실제 응답 전송 + 로컬 반영."""
    if body.status not in ("accepted", "declined", "tentative"):
        raise HTTPException(400, "status must be accepted|declined|tentative")
    e = s.get(CalEvent, eid)
    if not e:
        raise HTTPException(404, "event not found")
    acct = _account_for_event(s, eid)
    writer = _cal_writer(acct) if acct else None
    link = _event_link(s, eid)
    if acct and writer and link and hasattr(writer, "respond_to_event"):
        writer.respond_to_event(s, acct, link.external_id, body.status)
    e.response_status = body.status
    if acct:  # 참석자 목록에서 내 상태도 갱신
        myemail = (acct.external_account_id or "").lower()
        atts = list(e.attendees or [])
        for a in atts:
            if a.get("email", "").lower() == myemail:
                a["status"] = body.status
        e.attendees = atts
    s.add(e)
    s.commit()
    return _event_out(s, s.get(CalEvent, eid))


@router.delete("/calendar/events/{eid}")
def delete_event(eid: str, s: Session = Depends(get_session)):
    e = s.get(CalEvent, eid)
    if not e:
        raise HTTPException(404, "event not found")
    acct = _account_for_event(s, eid)
    writer = _cal_writer(acct) if acct else None
    link = _event_link(s, eid)
    if acct and writer and link:  # 연결된 캘린더(Google/Outlook)에서 실제 삭제
        writer.delete_remote_event(s, acct, link.external_id)
    if link:
        s.delete(link)
    s.delete(e)
    s.commit()
    return {"ok": True, "id": eid}
