# backend/app/models.py
# 주의: `from __future__ import annotations` 를 쓰지 않는다 —
# SQLAlchemy 2.x 가 Relationship 의 list["Project"] 문자열 주석을 해석하지 못해
# 매퍼 초기화에 실패한다(ForwardRef 로 평가되도록 둔다).
from datetime import UTC, date, datetime
from enum import Enum
from typing import Optional

from sqlalchemy import JSON, Column, Text
from sqlmodel import Field, Relationship, SQLModel


def now() -> datetime:
    return datetime.now(UTC)


# ---------- enums (값은 CONTRACT 고정) ----------
class TaskStatus(str, Enum):
    todo = "todo"
    doing = "doing"
    waiting = "waiting"
    review = "review"
    done = "done"


class Prio(str, Enum):
    high = "높음"
    normal = "보통"
    low = "낮음"


class InboxKind(str, Enum):
    text = "text"
    voice = "voice"
    image = "image"


class InboxStatus(str, Enum):
    new = "new"
    classified = "classified"
    confirmed = "confirmed"
    dismissed = "dismissed"


class ClsType(str, Enum):
    task = "task"
    event = "event"
    idea = "idea"


class Sphere(str, Enum):
    work = "work"
    life = "life"


# ---------- core ----------
class Person(SQLModel, table=True):
    __tablename__ = "person"
    id: str = Field(primary_key=True)  # 예: "jiwoo"
    name: str
    initial: str
    color: str  # 예: "var(--blue)" / "oklch(0.66 0.13 200)"
    is_me: bool = False
    email: str = ""  # phase-9 메일 발신자 주소
    role: str = ""  # phase-9 "프로덕트 디자이너" 등


class Folder(SQLModel, table=True):
    __tablename__ = "folder"
    id: str = Field(primary_key=True)  # "work" / "life"
    name: str  # "업무" / "개인"
    tone: str = "ink"  # tone 집합: blue|violet|coral|green|amber|ink|faint
    icon: str = "folder"  # "folder" / "heart"
    sort_order: int = 0
    is_system: bool = True

    projects: list["Project"] = Relationship(back_populates="folder")


class Project(SQLModel, table=True):
    __tablename__ = "project"
    id: str = Field(primary_key=True)  # "biz", "biz-okr", ...
    folder_id: str = Field(foreign_key="folder.id")
    parent_id: Optional[str] = Field(default=None, foreign_key="project.id")  # 무한 중첩
    name: str
    tone: str = "ink"
    sort_order: int = 0
    pinned: bool = False  # 즐겨찾기

    folder: Optional[Folder] = Relationship(back_populates="projects")
    parent: Optional["Project"] = Relationship(
        back_populates="children",
        sa_relationship_kwargs={"remote_side": "Project.id"},
    )
    children: list["Project"] = Relationship(back_populates="parent")
    tasks: list["Task"] = Relationship(back_populates="project")


class Task(SQLModel, table=True):
    __tablename__ = "task"
    id: str = Field(primary_key=True)  # "k1", "kx3", ...
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    project_id: str = Field(foreign_key="project.id")
    parent_id: Optional[str] = Field(default=None, foreign_key="task.id")  # 무한 중첩 하위작업
    title: str
    status: TaskStatus = TaskStatus.todo
    assignee_id: Optional[str] = Field(default=None, foreign_key="person.id")
    due: Optional[date] = None  # 시드는 "06-08" → 2026-06-08 로 적재
    prio: Prio = Prio.normal
    notes: str = ""  # HTML 허용
    est: str = ""
    delegated: bool = False
    sort_order: int = 0
    created_at: datetime = Field(default_factory=now)
    updated_at: datetime = Field(default_factory=now)

    project: Optional[Project] = Relationship(back_populates="tasks")
    parent: Optional["Task"] = Relationship(
        back_populates="children",
        sa_relationship_kwargs={"remote_side": "Task.id"},
    )
    children: list["Task"] = Relationship(back_populates="parent")
    comments: list["TaskComment"] = Relationship(back_populates="task")
    # phase-15: user_id 도 person FK 라 assignee 조인을 명시(다중 FK 모호성 해소)
    assignee: Optional[Person] = Relationship(
        sa_relationship_kwargs={"foreign_keys": "Task.assignee_id"}
    )


class TaskComment(SQLModel, table=True):
    __tablename__ = "task_comment"
    id: str = Field(primary_key=True)  # TEXT PK 예: "c1"
    task_id: str = Field(foreign_key="task.id")
    person_id: str = Field(foreign_key="person.id")
    text: str
    created_at: datetime = Field(default_factory=now)
    edited_at: Optional[datetime] = None  # 수정 시각(없으면 미수정)

    task: Optional[Task] = Relationship(back_populates="comments")
    person: Optional[Person] = Relationship()


# ---------- inbox (연합의 핵심) ----------
class InboxItem(SQLModel, table=True):
    __tablename__ = "inbox_item"
    id: str = Field(primary_key=True)  # "s1", ...
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    kind: InboxKind = InboxKind.text
    raw: str
    status: InboxStatus = InboxStatus.new
    created_at: datetime = Field(default_factory=now)
    materialized_task_id: Optional[str] = Field(default=None, foreign_key="task.id")

    classifications: list["InboxClassification"] = Relationship(back_populates="item")


class InboxClassification(SQLModel, table=True):
    __tablename__ = "inbox_classification"
    id: str = Field(primary_key=True)  # TEXT PK 예: "cls1"
    inbox_item_id: str = Field(foreign_key="inbox_item.id")  # 1:1 최신
    type: ClsType
    sphere: Sphere
    project_id: Optional[str] = Field(default=None, foreign_key="project.id")
    proj_label: str = ""  # 예: "개인 › 여행 — 한국"
    tone: str = "ink"
    due_text: str = ""  # 예: "출발 전 · ~6/14"
    when_text: str = ""  # 예: "오늘 21:00 빈 시간 추천"
    extra: str = ""  # 예: "가격 추적 알림 켜둠"
    reason: str = ""  # 사람이 읽는 한국어 설명
    confidence: float = 0.0
    model: str = ""  # 사용된 provider/model 표기
    created_at: datetime = Field(default_factory=now)

    item: Optional[InboxItem] = Relationship(back_populates="classifications")


# ---------- 대시보드 읽기전용 시드 ----------
class Event(SQLModel, table=True):
    __tablename__ = "event"
    id: str = Field(primary_key=True)  # TEXT PK 예: "e1"
    time: str
    title: str
    tag: str = ""
    dur: str = ""
    tone: str = "ink"  # tone 키 값('blue' 등). 'var(--blue)' 저장 금지
    soon: bool = False
    sort_order: int = 0


class Approval(SQLModel, table=True):
    """phase-2 읽기전용 → phase-7 '쓰기 가능'으로 확장.
    기존 필드는 유지하고 status/source/rule_id/created_at/executed_at/undone_at 추가."""

    __tablename__ = "approval"
    id: str = Field(primary_key=True)  # "a1", ..., 신규 "ap-xxxx"
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    icon: str
    tone: str
    risk: str  # "low" | "high"
    time: str = ""
    title: str
    detail: str = ""
    cta: str = ""
    alt: str = ""
    undo_label: str = ""
    sort_order: int = 0
    # --- 신규(phase-7) ---
    status: str = "pending"  # pending|approved|executed|undone
    source: str = "system"  # automation|agent|mail|calendar|finance|inbox|life|system
    rule_id: Optional[str] = Field(default=None, foreign_key="automation_rule.id")
    created_at: datetime = Field(default_factory=now)
    executed_at: Optional[datetime] = None
    undone_at: Optional[datetime] = None


class Goal(SQLModel, table=True):
    __tablename__ = "goal"
    id: str = Field(primary_key=True)  # TEXT PK 예: "g1"
    title: str
    pct: int
    sub: str = ""
    tone: str = "blue"  # tone 키 값('blue' 등). 'var(--blue)' 저장 금지
    sort_order: int = 0


class Briefing(SQLModel, table=True):
    """단일 row(id=1). 아침 브리핑 + 날씨/출근/수면 + saved_today/today_routed."""

    __tablename__ = "briefing"
    id: Optional[int] = Field(default=None, primary_key=True)  # 단일 row id=1
    today: str = ""  # 히어로 날짜 라벨 (예: "6월 7일 일요일")
    weather_temp: int = 0  # 현재 기온 (예: 24)
    weather_cond: str = ""  # "맑음 · 한낮 28°" — "한낮 28°"는 일 최고기온
    weather_icon: str = "cloudSun"  # 'cloudSun' 저장 → 표시 시 'sun' 으로 매핑
    commute: str = ""
    sleep: str = ""
    note: str = ""  # HTML 허용
    saved_today: str = ""  # "47분"
    today_routed: int = 0  # 7


# ==================== phase-7: 자율성 코어 (결재함 + 자동화) ====================
# enum 값은 영문(상태 머신/risk), 표시 라벨은 프론트가 매핑.


class RiskLevel(str, Enum):
    low = "low"  # 되돌릴 수 있는 일 — mixed 이상 자동 실행
    high = "high"  # 보내기·결제·타인 전달 — 항상 승인 대기


class ApprovalStatus(str, Enum):
    pending = "pending"  # 승인 대기
    approved = "approved"  # high 승인 직후(표시용)
    executed = "executed"  # 실행 완료(자동/승인)
    undone = "undone"  # 되돌림


class ApprovalSource(str, Enum):
    automation = "automation"
    agent = "agent"
    mail = "mail"
    calendar = "calendar"
    finance = "finance"
    inbox = "inbox"
    life = "life"
    system = "system"


class AutonomyLevel(str, Enum):
    approval_first = "approval_first"  # 승인 우선 (원본 id "approve")
    mixed = "mixed"  # 혼합 (추천)
    full_auto = "full_auto"  # 완전 자율 (원본 id "auto")


class AutonomySetting(SQLModel, table=True):
    """단일 row(id="default"). 데모 단일 사용자(지우)의 자율성 레벨."""

    __tablename__ = "autonomy_setting"
    id: str = Field(default="default", primary_key=True)  # 단일 row id="default" (TEXT PK)
    level: str = AutonomyLevel.mixed.value  # 기본 '혼합'(원본 추천)
    updated_at: datetime = Field(default_factory=now)


class ApprovalLog(SQLModel, table=True):
    """결재함 활동 로그 — '밤사이 조용히 한 일'. approve-data.js log 이식."""

    __tablename__ = "approval_log"
    id: str = Field(primary_key=True)  # "alog1"...
    time: str  # 표시용("08:55","어제 23:10")
    text: str
    approval_id: Optional[str] = Field(default=None, foreign_key="approval.id")
    sort_order: int = 0
    created_at: datetime = Field(default_factory=now)


class AutomationRule(SQLModel, table=True):
    __tablename__ = "automation_rule"
    id: str = Field(primary_key=True)  # "r1"~"r7", 신규 "rule-xxxx"
    name: str
    cat: str  # mail | cal | focus | life
    trigger: str
    cond: Optional[str] = None  # None 이면 조건 칩 미표시
    action: str
    on: bool = True  # 켜짐/꺼짐
    last: str = ""  # "오늘 09:12"/"방금 만듦"/"지난주 금"
    runs: int = 0  # 이번 주 실행 횟수
    fresh: bool = False  # 막 생성됨 → 프론트 rl-new 애니메이션
    source: str = "user"  # user | suggestion | example
    created_at: datetime = Field(default_factory=now)


class AutomationSuggestion(SQLModel, table=True):
    __tablename__ = "automation_suggestion"
    id: str = Field(primary_key=True)  # "asug-s1"... (시드 원본 s1/s2)
    pattern: str  # "월요일 아침마다 ..."
    offer_name: str
    offer_cat: str
    offer_trigger: str
    offer_cond: Optional[str] = None
    offer_action: str
    status: str = "open"  # open | accepted | dismissed
    created_at: datetime = Field(default_factory=now)


class AutomationRunLog(SQLModel, table=True):
    __tablename__ = "automation_run_log"
    id: str = Field(primary_key=True)  # "arun1"...
    time: str  # "12:48"/"어제 18:30"
    rule_id: Optional[str] = Field(default=None, foreign_key="automation_rule.id")
    rule: str  # 규칙명 스냅샷("영수증 자동 정리")
    text: str  # "점심 결제 13,500원 → 식비로 분류"
    undone: bool = False
    sort_order: int = 0
    created_at: datetime = Field(default_factory=now)


class AutomationStats(SQLModel, table=True):
    """단일 row(id=1). 페이지 eyebrow 통계. auto-data.js stats 이식."""

    __tablename__ = "automation_stats"
    id: Optional[int] = Field(default=None, primary_key=True)  # 단일 row id=1
    active: int = 0  # 활성 규칙 수(파생; 시드값 7)
    runs_week: int = 0  # 이번 주 실행(시드 31)
    saved: str = ""  # "1시간 40분"


# ==================== phase-8: 일정 + 회의 도우미 ====================


class Calendar(SQLModel, table=True):
    """캘린더 카테고리 (cal-data.js: cals)."""

    __tablename__ = "calendar"
    id: str = Field(primary_key=True)  # work/meeting/personal/team/health
    name: str  # 업무/미팅/개인/팀/건강
    tone: str = "ink"  # tone 집합
    on: bool = True  # 기본 표시 (프론트는 localStorage 로 토글 영속)
    sort_order: int = 0


class CalEvent(SQLModel, table=True):
    """캘린더 이벤트 (cal-data.js: events) — 시간 그리드용. 대시보드 event 와 별개 테이블."""

    __tablename__ = "cal_event"
    id: str = Field(primary_key=True)  # "e1".."e22"
    day: int  # 날짜의 '일'(1..31) — 데모 호환. 실데이터는 date 우선.
    date: str = ""  # 실제 날짜 ISO "YYYY-MM-DD"(외부 동기화). 비면 데모(6월)로 간주.
    start: str  # "HH:MM"
    end: str  # "HH:MM"
    title: str
    cal: str = Field(foreign_key="calendar.id")  # 카테고리
    loc: str = ""
    note: str = ""
    soon: bool = False  # '곧' 배지
    people: str = ""  # 콤마구분 문자열(표시용 호환)
    rrule: str = ""  # iCalendar RRULE (반복 일정), 예 "FREQ=WEEKLY;BYDAY=MO,WE"
    reminders: list = Field(default_factory=list, sa_column=Column(JSON))  # [{minutes:int}]
    attendees: list = Field(default_factory=list, sa_column=Column(JSON))  # [{email,name,status}]
    response_status: str = ""  # 내 RSVP: accepted|declined|tentative|needsAction
    sort_order: int = 0


class FocusBlock(SQLModel, table=True):
    """집중 모드 블록 (cal-data.js: focusBlocks)."""

    __tablename__ = "focus_block"
    id: str = Field(primary_key=True)  # "f1".."f4"
    day: int
    start: str
    end: str
    title: str
    type: str = "light"  # "light" | "deep"
    tag: str = ""
    task_id: Optional[str] = Field(default=None, foreign_key="task.id")
    auto: bool = True  # 아리 자동 배치 여부
    sort_order: int = 0


class MeetPhase(str, Enum):
    done = "done"
    live = "live"
    upcoming = "upcoming"


class Meeting(SQLModel, table=True):
    """회의 (cal-data.js: meets[evId]). 표시 전용 중첩 데이터는 *_json TEXT 로 저장."""

    __tablename__ = "meeting"
    id: str = Field(primary_key=True)  # event id 와 동일 키 ("e3","e4","e6","e7")
    event_id: str = Field(foreign_key="cal_event.id")
    phase: MeetPhase = MeetPhase.upcoming
    one_on_one: bool = False
    meta: str = ""
    starts_in: str = ""
    summary_json: str = "[]"
    decisions_json: str = "[]"
    agenda_json: str = "[]"
    last_meeting_json: str = "{}"
    last_actions_json: str = "[]"
    insights_json: str = "[]"
    docs_json: str = "[]"
    person_json: str = "{}"
    promises_json: str = "[]"
    signals_json: str = "[]"
    talking_points_json: str = "[]"

    actions: list["MeetingAction"] = Relationship(back_populates="meeting")


class MeetingAction(SQLModel, table=True):
    """회의/이벤트 액션 아이템 (작업으로 실체화 대상)."""

    __tablename__ = "meeting_action"
    id: str = Field(primary_key=True)  # "ma1"/"ea1"...
    meeting_id: Optional[str] = Field(default=None, foreign_key="meeting.id")
    event_id: Optional[str] = Field(default=None, foreign_key="cal_event.id")
    source: str = "meeting"  # "meeting" | "event"
    idx: int = 0  # 회의 내 순번
    text: str
    who: str = "나"  # 담당자 표시명 (나/현우/수아...)
    when_text: str = ""  # 오늘/내일/이번 주
    added: bool = False  # 작업으로 보냈는가
    materialized_task_id: Optional[str] = Field(default=None, foreign_key="task.id")
    sort_order: int = 0

    meeting: Optional[Meeting] = Relationship(back_populates="actions")


# ==================== phase-9: 메일 + 알림 ====================


class MailPriority(str, Enum):
    high = "높음"
    normal = "보통"
    low = "낮음"


class NotifyBucket(str, Enum):
    now = "now"  # 지금 (직접 봐야 함)
    later = "later"  # 나중에 (묶어서 다이제스트)
    held = "held"  # 아리가 처리 (자동 · 되돌리기 가능)


# ---------- 메일 (3계정 통합) ----------
class MailAccount(SQLModel, table=True):
    __tablename__ = "mail_account"
    id: str = Field(primary_key=True)  # work | personal | side
    name: str
    email: str
    tone: str = "ink"
    kind: str = ""  # "Lumi Workspace" | "Google" | "HEY"
    unread: int = 0
    connector: str = "mock"
    sort_order: int = 0


class MailFolder(SQLModel, table=True):
    __tablename__ = "mail_folder"
    id: str = Field(primary_key=True)  # inbox|starred|sent|drafts|archive
    name: str
    icon: str = "inbox"
    sort_order: int = 0


class AccountFolder(SQLModel, table=True):
    """계정별 실제 폴더/라벨(Gmail 라벨·Outlook 메일폴더)을 sync 때 미러링.
    사이드바 트리가 계정마다 진짜 폴더 구조를 보여주는 원천."""

    __tablename__ = "account_folder"
    id: str = Field(primary_key=True)  # f"{mail_account}:{slug}"
    mail_account: str = Field(foreign_key="mail_account.id", index=True)
    slug: str  # 필터 키: inbox/spam/trash/archive/sent | cat:promotions | label:<name> | of:<id>
    name: str  # 표시명(시스템은 한글, 사용자 라벨/커스텀 폴더는 원본)
    kind: str = "system"  # system | category | label | custom
    icon: str = "folder"
    provider_id: str = ""  # 원격 폴더/라벨 id(Outlook folderId / Gmail labelId)
    unread: int = 0  # 안읽음 수(Outlook=서버값, Gmail=동기화분 기준)
    total: int = 0
    sort_order: int = 0


class Email(SQLModel, table=True):
    __tablename__ = "email"
    id: str = Field(primary_key=True)  # "m1".."m8"
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    account: str = Field(foreign_key="mail_account.id")
    from_key: str  # people 키 ("hyunwoo" 등). 'from'은 예약어라 from_key
    to: str = ""
    cc: str = ""  # 받은 메일의 참조(콤마 구분)
    thread_id: str = ""  # Gmail threadId / Outlook conversationId (대화 묶기)
    subject: str = ""
    time: str = ""  # "8분 전"
    date: str = ""  # "오늘 09:12"
    read: bool = False
    starred: bool = False
    archived: bool = False
    has_attach: bool = False
    labels: list = Field(default_factory=list, sa_column=Column(JSON))
    preview: str = ""
    body: list = Field(default_factory=list, sa_column=Column(JSON))
    # 원본 HTML 본문(샌드박스 iframe 렌더용; cid: 인라인 이미지는 읽기 시 첨부 URL 로 치환).
    body_html: str = Field(default="", sa_column=Column(Text))
    attachments: list = Field(default_factory=list, sa_column=Column(JSON))
    ai_json: dict = Field(default_factory=dict, sa_column=Column(JSON))
    sort_order: int = 0
    # 실제 수신 시각(Gmail internalDate / Outlook receivedDateTime). 목록 정렬(최신순)·표시 기준.
    received_at: datetime | None = Field(default=None, index=True)
    # 메일이 속한 폴더(provider 기준 1개). inbox/spam/trash/archive/sent | of:<id>(Outlook 커스텀).
    folder: str = Field(default="inbox", index=True)
    folder_name: str = ""  # Outlook 커스텀 폴더 표시명(시스템 폴더는 비움 → 표준 한글명 사용)
    # Outlook 집중 받은편지함 분류(focused/other). 빈 문자열=미분류(Gmail 등). 받은편지함=focused.
    inference: str = ""
    created_at: datetime = Field(default_factory=now)


class Sent(SQLModel, table=True):
    __tablename__ = "sent"
    id: str = Field(primary_key=True)
    account: str = Field(foreign_key="mail_account.id")
    to: str = ""
    to_key: str = ""
    subject: str = ""
    time: str = ""
    date: str = ""
    preview: str = ""
    sort_order: int = 0
    created_at: datetime = Field(default_factory=now)


class OutboundMail(SQLModel, table=True):
    """phase-16: 발송 대기 메일. /mail/send 가 결재와 함께 생성, 승인 시 실제 발송.
    connector_account_id 가 real+connected 면 Gmail/Graph 로 발송, 아니면 mock(Sent 행)."""

    __tablename__ = "outbound_mail"
    id: str = Field(primary_key=True)  # "ob-xxxx"
    approval_id: str = Field(foreign_key="approval.id", index=True)
    connector_account_id: Optional[str] = Field(default=None, foreign_key="connector_account.id")
    mail_account: str = ""  # 발신 MailAccount id(work|personal|ma-xxxx)
    to: str = ""
    cc: str = ""  # 참조(콤마 구분)
    bcc: str = ""  # 숨은참조(콤마 구분)
    subject: str = ""
    body: str = ""
    # [{name,mime,data_b64}]
    attachments: list = Field(default_factory=list, sa_column=Column(JSON))
    status: str = "pending"  # pending | sent | failed | mock
    error_detail: str = ""
    created_at: datetime = Field(default_factory=now)
    sent_at: Optional[datetime] = None


class Draft(SQLModel, table=True):
    __tablename__ = "draft"
    id: str = Field(primary_key=True)
    account: str = Field(foreign_key="mail_account.id")
    to: str = ""
    subject: str = ""
    time: str = ""
    preview: str = ""
    body: str = ""
    sort_order: int = 0
    created_at: datetime = Field(default_factory=now)


# ---------- 알림 트리아지 ----------
class Notification(SQLModel, table=True):
    __tablename__ = "notification"
    id: str = Field(primary_key=True)  # "n1".., "l1".., "h1"..
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    bucket: NotifyBucket = NotifyBucket.now
    src: str = "app"  # mail|cal|chat|fin|app
    app: str = ""
    title: str = ""
    sum: str = ""  # 본문 요약 (원본 키 'sum' 보존)
    time: str = ""
    why: str = ""  # 분류 이유 칩 텍스트
    tone: str = "ink"  # 이유 칩 색 (muted 포함)
    act: str = ""  # 주요 액션 라벨 (now 전용)
    held_by_rule: str = ""  # held 일 때 적용된 sender_rule id
    sort_order: int = 0
    created_at: datetime = Field(default_factory=now)


class NotificationGuard(SQLModel, table=True):
    """단일 row(id=1). 집중 보호 + 조용한 시간."""

    __tablename__ = "notification_guard"
    id: Optional[int] = Field(default=None, primary_key=True)
    on: bool = True
    until: str = "16:00"
    held: int = 4
    quiet: str = "22:30 – 07:00"


class Digest(SQLModel, table=True):
    __tablename__ = "digest"
    id: str = Field(primary_key=True)  # "dg-am" 등
    time: str = ""
    name: str = ""
    desc: str = ""
    count: int = 0
    done: bool = False
    sort_order: int = 0


class SenderRule(SQLModel, table=True):
    __tablename__ = "sender_rule"
    id: str = Field(primary_key=True)  # "sr1"..
    who: str = ""
    from_label: str = ""  # 원본 'from'
    rule: str = ""
    bucket: NotifyBucket = NotifyBucket.now
    tone: str = "ink"
    sort_order: int = 0


class NotifyStats(SQLModel, table=True):
    """단일 row(id=1). 오늘 요약."""

    __tablename__ = "notify_stats"
    id: Optional[int] = Field(default=None, primary_key=True)
    total: int = 0
    seen: int = 0
    filtered: str = ""
    later_more: int = 0
    held_more: int = 0


# ==================== phase-10: 리서치 + 여행 ====================


class SourceKind(str, Enum):
    pdf = "pdf"
    web = "web"
    note = "note"


class ResearchCollection(SQLModel, table=True):
    __tablename__ = "research_collection"
    id: str = Field(primary_key=True)  # chip/onb/stroller
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    name: str
    tone: str = "ink"
    n: int = 0
    sort_order: int = 0


class ResearchSource(SQLModel, table=True):
    __tablename__ = "research_source"
    id: str = Field(primary_key=True)  # s1..s5
    kind: SourceKind
    title: str
    from_label: str = ""  # 원본 from (응답 alias "from")
    col: Optional[str] = Field(default=None, foreign_key="research_collection.id")
    learned: bool = False
    sort_order: int = 0


class ResearchReport(SQLModel, table=True):
    __tablename__ = "research_report"
    id: str = Field(primary_key=True)  # rp1
    title: str
    asked: str = ""
    meta: str = ""
    synthesis: str = ""  # HTML
    note: str = ""
    counts: list = Field(default_factory=list, sa_column=Column(JSON))
    cross: list = Field(default_factory=list, sa_column=Column(JSON))
    collection_id: Optional[str] = Field(default=None, foreign_key="research_collection.id")
    created_at: datetime = Field(default_factory=now)


class ResearchQA(SQLModel, table=True):
    __tablename__ = "knowledge_qa"  # CONTRACT 명명
    id: str = Field(primary_key=True)  # qa1
    q: str
    a: str = ""  # HTML
    refs: list = Field(default_factory=list, sa_column=Column(JSON))
    collection_id: Optional[str] = Field(default=None, foreign_key="research_collection.id")
    created_at: datetime = Field(default_factory=now)


class ResearchChart(SQLModel, table=True):
    __tablename__ = "research_chart"
    id: str = Field(primary_key=True)  # ch1
    title: str
    asked: str = ""
    unit: str = ""
    bars: list = Field(default_factory=list, sa_column=Column(JSON))
    insight: str = ""  # HTML
    caution: str = ""


class RagChunk(SQLModel, table=True):
    """ingest 된 자료의 청크 + 임베딩(vector store)."""

    __tablename__ = "rag_chunk"
    id: str = Field(primary_key=True)  # chk_s1_0
    source_id: str = Field(foreign_key="research_source.id")
    seq: int = 0
    text: str
    part_label: str = ""
    embedding: list = Field(default_factory=list, sa_column=Column(JSON))
    dim: int = 0
    model: str = ""
    created_at: datetime = Field(default_factory=now)


# ---------- 여행 ----------
class Trip(SQLModel, table=True):
    __tablename__ = "trip"
    id: str = Field(primary_key=True)  # tp_busan
    user_id: str = Field(default="jiwoo", foreign_key="person.id", index=True)  # phase-15 소유자
    dday: str = ""
    title: str = ""
    dates: str = ""
    purpose: str = ""
    brief: str = ""
    weather: list = Field(default_factory=list, sa_column=Column(JSON))
    expense_budget: int = 0
    expense_planned: int = 0
    expense_note: str = ""
    expense_rows: list = Field(default_factory=list, sa_column=Column(JSON))


class TripRoute(SQLModel, table=True):
    __tablename__ = "trip_route"
    id: str = Field(primary_key=True)  # rt_out/rt_back
    trip_id: str = Field(foreign_key="trip.id")
    dir: str  # out | back
    mode: str
    from_st: str
    ft: str
    to_st: str
    tt: str
    seat: str = ""
    note: str = ""


class TripStay(SQLModel, table=True):
    __tablename__ = "trip_stay"
    id: str = Field(primary_key=True)
    trip_id: str = Field(foreign_key="trip.id")
    name: str
    desc: str = ""
    conf: str = ""


class TripPrep(SQLModel, table=True):
    __tablename__ = "trip_prep"
    id: str = Field(primary_key=True)
    trip_id: str = Field(foreign_key="trip.id")
    text: str
    state: str = "done"  # done | doing
    sort_order: int = 0


class TripDay(SQLModel, table=True):
    __tablename__ = "trip_day"
    id: str = Field(primary_key=True)  # d1/d2
    trip_id: str = Field(foreign_key="trip.id")
    tab: str
    items: list = Field(default_factory=list, sa_column=Column(JSON))
    sort_order: int = 0


class TripChecklist(SQLModel, table=True):
    __tablename__ = "trip_checklist"
    id: str = Field(primary_key=True)  # c1..c7
    trip_id: str = Field(foreign_key="trip.id")
    group: str
    text: str
    auto: str = ""
    sort_order: int = 0


class SavedTrip(SQLModel, table=True):
    __tablename__ = "saved_trip"
    id: str = Field(primary_key=True)  # sv1..sv3
    title: str
    tag: str = ""
    note: str = ""
    now: str = ""  # "89,000원"
    delta: str = ""
    down: bool = True
    watch: bool = True
    hint: str = ""
    spark: list = Field(default_factory=list, sa_column=Column(JSON))
    sort_order: int = 0


class TripPlan(SQLModel, table=True):
    __tablename__ = "trip_plan"
    id: str = Field(primary_key=True)  # plan_jeju/plan_tokyo
    example: str
    idx: int = 0
    title: str
    meta: str = ""
    summary: str = ""  # HTML
    weather: str = ""
    transport: list = Field(default_factory=list, sa_column=Column(JSON))
    stay: list = Field(default_factory=list, sa_column=Column(JSON))
    days: list = Field(default_factory=list, sa_column=Column(JSON))
    budget: dict = Field(default_factory=dict, sa_column=Column(JSON))
    checklist: list = Field(default_factory=list, sa_column=Column(JSON))
    sources: list = Field(default_factory=list, sa_column=Column(JSON))


# ==================== phase-11: 라이프 케어 (건강·금융·지식) ====================


class ConnectorDomain(str, Enum):
    health = "health"
    finance = "finance"
    knowledge = "knowledge"
    # phase-13: 외부 연동 도메인 확장(mail/calendar/chat)
    mail = "mail"
    calendar = "calendar"
    chat = "chat"


class KnowledgeType(str, Enum):
    article = "article"
    note = "note"
    idea = "idea"
    highlight = "highlight"


class InsightKind(str, Enum):
    alert = "alert"
    save = "save"
    info = "info"


class ConnectorSource(SQLModel, table=True):
    """좌측 레일 '연결된 데이터'. life-data.js sources 그대로."""

    __tablename__ = "connector_source"
    id: str = Field(primary_key=True)  # apple/watch/fit/woori/...
    domain: ConnectorDomain
    name: str
    kind: str
    tone: str = "ink"
    on: bool = True
    last: str = ""
    sort_order: int = 0


class HealthRing(SQLModel, table=True):
    __tablename__ = "health_ring"
    id: str = Field(primary_key=True)
    label: str
    val: str
    unit: str = ""
    goal: str
    pct: int = 0
    icon: str
    tone: str = "coral"
    sort_order: int = 0


class HealthVital(SQLModel, table=True):
    __tablename__ = "health_vital"
    id: str = Field(primary_key=True)
    label: str
    val: str
    unit: str = ""
    trend: str = "flat"  # up|down|flat
    note: str = ""
    tone: str = "coral"
    sort_order: int = 0


class SleepNight(SQLModel, table=True):
    """단일 row(id=1). 어젯밤 수면 + 7일 추이."""

    __tablename__ = "sleep_night"
    id: Optional[int] = Field(default=None, primary_key=True)
    total: str = ""
    score: int = 0
    stages_json: str = "[]"
    week_json: str = "[]"
    note: str = ""


class HealthHabit(SQLModel, table=True):
    __tablename__ = "health_habit"
    id: str = Field(primary_key=True)  # water/stretch/vitamin/meditate
    title: str
    goal: str
    done: int = 0
    total: int = 1
    streak: int = 0
    icon: str
    tone: str = "blue"
    at: str = ""
    automation_rule_id: Optional[str] = Field(default=None, foreign_key="automation_rule.id")
    sort_order: int = 0


class FinanceBudget(SQLModel, table=True):
    """단일 row(id=1). 이달 예산."""

    __tablename__ = "finance_budget"
    id: Optional[int] = Field(default=None, primary_key=True)
    spent: int = 0
    limit: int = 0
    pct: int = 0
    delta_pct: int = 0
    days_left: int = 0


class FinanceCategory(SQLModel, table=True):
    __tablename__ = "finance_category"
    id: str = Field(primary_key=True)
    name: str
    amt: int = 0
    pct: int = 0
    icon: str
    tone: str = "coral"
    over: bool = False
    sort_order: int = 0


class FinanceSub(SQLModel, table=True):
    __tablename__ = "finance_sub"
    id: str = Field(primary_key=True)
    name: str
    date: str = ""
    in_days: int = 0
    amt: int = 0
    icon: str
    tone: str = "coral"
    note: str = ""
    paused: bool = False
    sort_order: int = 0


class FinanceInsight(SQLModel, table=True):
    __tablename__ = "finance_insight"
    id: str = Field(primary_key=True)  # ins-shop/ins-netflix/ins-save
    kind: InsightKind
    icon: str
    title: str
    body: str
    action: str = ""  # pause_sub|raise_budget|auto_transfer|""
    action_ref: str = ""
    sort_order: int = 0


class KnowledgeCollection(SQLModel, table=True):
    __tablename__ = "knowledge_collection"
    id: str = Field(primary_key=True)
    name: str
    count: int = 0
    tone: str = "blue"
    sort_order: int = 0


class KnowledgeItem(SQLModel, table=True):
    """제2의 뇌 스크랩. phase-10 research_source 와 공유 가능."""

    __tablename__ = "knowledge_item"
    id: str = Field(primary_key=True)  # k1..k5
    type: KnowledgeType
    title: str
    src: str = ""
    time: str = ""
    tone: str = "blue"
    icon: str
    tags_json: str = "[]"
    excerpt: str = ""
    ai: str = ""  # 아리 제안(HTML)
    collection_id: Optional[str] = Field(default=None, foreign_key="knowledge_collection.id")
    connector_id: Optional[str] = Field(default=None, foreign_key="connector_source.id")
    created_at: datetime = Field(default_factory=now)
    sort_order: int = 0


# ==================== phase-12: 여정 ====================


class JourneyLink(SQLModel, table=True):
    """여정 보드의 작업↔사람↔미팅 연결선. journey-clean-data.js links 이식."""

    __tablename__ = "journey_link"
    id: str = Field(primary_key=True)  # jl1..
    from_node: str
    to_node: str
    tone: str = "ink"
    dash: bool = False
    sort_order: int = 0


# ==================== phase-13: 외부 연동 & 데이터 커넥터 ====================
class ConnectorMode(str, Enum):
    mock = "mock"
    csv = "csv"
    healthkit = "healthkit"
    ics = "ics"
    real = "real"


class ConnState(str, Enum):
    connected = "connected"  # 정상 연결 (라이프 카드 on:true)
    disconnected = "disconnected"  # 미연결 ("연결 안 됨")
    syncing = "syncing"  # 동기화 중
    error = "error"  # 마지막 sync 실패
    token_expired = "token_expired"  # 권한 만료 → 재인증 필요


class ConnectorAccount(SQLModel, table=True):
    """한 도메인의 한 제공자 계정. 예: mail/work(jiwoo@lumi.co), finance/woori."""

    __tablename__ = "connector_account"
    id: str = Field(primary_key=True)  # "ca-mail-work"
    user_id: str = Field(default="jiwoo", foreign_key="person.id")  # 단일 데모(phase-15 멀티유저)
    domain: ConnectorDomain
    mode: ConnectorMode = ConnectorMode.mock
    provider: str = ""  # "gmail" | "google_calendar" | "notion" | "woori" ...
    external_account_id: str = ""  # 제공자 계정 식별자
    name: str = ""  # 표시명 "회사" / "우리카드"
    kind: str = ""  # "Lumi Workspace" / "신용/체크"
    tone: str = "ink"  # blue|violet|coral|green|amber|ink|faint
    state: ConnState = ConnState.disconnected
    last_synced_at: Optional[datetime] = None
    last_label: str = "연결 안 됨"  # 라이프 카드 last 문구
    token_enc: str = ""  # 암호화된 토큰 JSON. 평문 저장 금지.
    scopes: str = ""  # 콤마구분 OAuth 스코프
    error_detail: str = ""
    created_at: datetime = Field(default_factory=now)
    updated_at: datetime = Field(default_factory=now)


class ConnectorSyncState(SQLModel, table=True):
    """증분 sync 커서. 제공자별 델타 토큰 또는 since 타임스탬프."""

    __tablename__ = "connector_sync_state"
    id: str = Field(primary_key=True)  # "cs-<account_id>"
    account_id: str = Field(foreign_key="connector_account.id")
    cursor: str = ""  # Gmail historyId / Google syncToken / ISO since
    page_token: str = ""  # 진행 중 페이지네이션 토큰(중단 복구용)
    last_full_sync_at: Optional[datetime] = None
    last_delta_sync_at: Optional[datetime] = None
    items_seen: int = 0
    items_upserted: int = 0


class ExternalLink(SQLModel, table=True):
    """외부 제공자 객체 ↔ 내부 엔티티 매핑(중복/충돌 처리의 핵심).
    (account_id, external_id) UNIQUE 로 멱등 upsert."""

    __tablename__ = "external_link"
    id: str = Field(primary_key=True)  # "xl-<hash>"
    account_id: str = Field(foreign_key="connector_account.id", index=True)
    external_id: str = Field(index=True)  # 제공자측 고유 id
    entity_type: str  # "email" | "event" | "finance_tx" | "health_sample" | ...
    entity_id: str  # 내부 엔티티 PK
    etag: str = ""  # 변경 감지용
    external_updated_at: Optional[datetime] = None
    created_at: datetime = Field(default_factory=now)


class OAuthState(SQLModel, table=True):
    """OAuth Authorization Code 플로우의 CSRF state + PKCE verifier 임시 저장."""

    __tablename__ = "oauth_state"
    id: str = Field(primary_key=True)  # state nonce 자체를 PK 로
    domain: ConnectorDomain
    provider: str
    code_verifier: str = ""  # PKCE
    redirect_after: str = "/life"  # 콜백 후 프론트 복귀 경로
    created_at: datetime = Field(default_factory=now)


class ConnectorAccountLog(SQLModel, table=True):
    """연결/해제/동기화/오류 활동 로그."""

    __tablename__ = "connector_account_log"
    id: str = Field(primary_key=True)  # "cl-xxxx"
    account_id: str = Field(foreign_key="connector_account.id")
    action: str  # "connected"|"disconnected"|"sync"|"error"|"token_refreshed"|"import"
    detail: str = ""
    items: int = 0
    created_at: datetime = Field(default_factory=now)


class FinanceTx(SQLModel, table=True):
    """금융 거래(카드 명세/계좌 입출금). CSV 임포트·아그리게이터 sync 의 write 타깃."""

    __tablename__ = "finance_tx"
    id: str = Field(primary_key=True)  # "ftx-xxxx"
    account_id: str = Field(default="", foreign_key="connector_account.id")
    date: str = ""  # "2026-06-08" 또는 "오늘"
    merchant: str = ""
    amount: int = 0  # 정수 원화(음수=환불/입금)
    category: str = ""  # 식비/쇼핑/교통/카페·간식/구독/문화·여가
    category_id: str = ""  # finance_category.id (있으면)
    account_label: str = ""  # "우리카드" 등
    created_at: datetime = Field(default_factory=now)


class HealthSample(SQLModel, table=True):
    """건강 샘플(걸음/수면/심박 등). HealthKit export·Google Fit sync 의 write 타깃."""

    __tablename__ = "health_sample"
    id: str = Field(primary_key=True)  # "hs-xxxx"
    account_id: str = Field(default="", foreign_key="connector_account.id")
    kind: str = ""  # "steps" | "sleep" | "heart_rate" | "exercise"
    value: float = 0.0
    unit: str = ""
    day: str = ""  # "2026-06-08"
    created_at: datetime = Field(default_factory=now)


class ChatMessage(SQLModel, table=True):
    """메신저 인바운드 메시지. chat webhook 의 write 타깃 → notification 트리아지."""

    __tablename__ = "chat_message"
    id: str = Field(primary_key=True)  # "cm-xxxx"
    account_id: str = Field(default="", foreign_key="connector_account.id")
    sender: str = ""
    text: str = ""
    channel: str = ""
    at: str = ""
    created_at: datetime = Field(default_factory=now)


# ==================== phase-14: 능동 에이전트 + 멀티모달 ====================
class ErrandKind(str, Enum):
    booking = "booking"  # 예약(병원/식당/수리)
    refund = "refund"  # 환불 요청
    cancel = "cancel"  # 구독 해지/일시정지 (a6 Netflix)
    support = "support"  # 고객센터 문의


class ErrandStatus(str, Enum):
    planning = "planning"
    running = "running"
    awaiting_approval = "awaiting_approval"  # external_effect → 결재함 대기
    done = "done"
    failed = "failed"
    cancelled = "cancelled"


class StepState(str, Enum):
    pending = "pending"
    running = "running"
    done = "done"
    blocked = "blocked"  # 승인 대기로 막힘
    error = "error"


class ProactiveKind(str, Enum):
    schedule = "schedule"  # 미팅 연속 → 점심 보호
    finance = "finance"  # 청구서 이상
    focus = "focus"  # 딥워크 보호
    health = "health"  # 건강 코칭
    digest = "digest"  # 다이제스트/브리핑


class ErrandTask(SQLModel, table=True):
    __tablename__ = "errand_task"
    id: str = Field(primary_key=True)  # "er1".."erN"
    kind: str = ErrandKind.cancel.value
    title: str
    goal: str = ""
    target: Optional[str] = None
    tone: str = "amber"
    status: str = ErrandStatus.planning.value
    autonomy_at_start: str = "mixed"  # 시작 시점 자율성 스냅샷
    approval_id: Optional[str] = Field(default=None, foreign_key="approval.id")
    result_summary: Optional[str] = None
    model: str = ""  # "agent:<model>" | "scripted"
    created_at: datetime = Field(default_factory=now)
    updated_at: datetime = Field(default_factory=now)


class ErrandStep(SQLModel, table=True):
    """plan→act(tool)→observe→reflect 한 스텝. 타임라인의 1행."""

    __tablename__ = "errand_step"
    id: str = Field(primary_key=True)  # "er1-s1"
    errand_id: str = Field(foreign_key="errand_task.id", index=True)
    seq: int
    phase: str  # "plan"|"act"|"observe"|"reflect"
    tool: Optional[str] = None
    label: str
    detail: Optional[str] = None
    external_effect: bool = False  # True면 결재함 게이트(발송/결제/해지)
    state: str = StepState.pending.value
    created_at: datetime = Field(default_factory=now)


class ProactiveCard(SQLModel, table=True):
    __tablename__ = "proactive_card"
    id: str = Field(primary_key=True)  # "pc1".."pcN"
    kind: str = ProactiveKind.schedule.value
    icon: str = "spark"
    tone: str = "violet"
    title: str
    why: str = ""
    cta: Optional[str] = None
    action_kind: Optional[str] = None  # focus_block|task|automation|approval|none
    action_payload: Optional[str] = None  # JSON(text)
    detected_at: str = "방금"
    status: str = "active"  # active|accepted|dismissed
    created_at: datetime = Field(default_factory=now)


class WeeklyReview(SQLModel, table=True):
    __tablename__ = "weekly_review"
    id: str = Field(primary_key=True)  # "wr-2026-w24"
    week_label: str
    deep_work_h: float = 0.0
    meeting_h: float = 0.0
    done_count: int = 0
    auto_count: int = 0
    saved_minutes: int = 0
    patterns_json: str = "[]"  # [{title, suggest, action_kind, payload}]
    note: Optional[str] = None
    created_at: datetime = Field(default_factory=now)


# ==================== phase-15: 인증/멀티유저 + 감사 ====================
# person(=user)을 인증 주체로 쓰고 자격만 분리(시드 person 보존). per-user 스코프는
# 기존 테이블에 user_id 컬럼(기본 "jiwoo")을 더해 일반화한다.
class UserCredential(SQLModel, table=True):
    __tablename__ = "user_credential"
    id: str = Field(primary_key=True)  # "uc-jiwoo"
    user_id: str = Field(foreign_key="person.id", index=True, unique=True)
    email: str = Field(index=True, unique=True)  # 로그인 식별자
    password_hash: str  # pbkdf2(salt$hash) — 평문 저장 금지
    is_active: bool = True
    role: str = "member"  # member | admin
    created_at: datetime = Field(default_factory=now)
    last_login_at: Optional[datetime] = None


class AuthSession(SQLModel, table=True):
    __tablename__ = "auth_session"
    id: str = Field(primary_key=True)  # 세션 id(랜덤 hex)
    user_id: str = Field(foreign_key="person.id", index=True)
    created_at: datetime = Field(default_factory=now)
    expires_at: datetime
    revoked: bool = False
    user_agent: Optional[str] = None
    ip: Optional[str] = None


class ApiToken(SQLModel, table=True):
    __tablename__ = "api_token"
    id: str = Field(primary_key=True)  # "tok-xxxx"
    user_id: str = Field(foreign_key="person.id", index=True)
    name: str
    token_hash: str  # sha256(token) — 평문 저장 금지
    created_at: datetime = Field(default_factory=now)
    last_used_at: Optional[datetime] = None
    revoked: bool = False


class AuditLog(SQLModel, table=True):
    """보안 감사 로그(인증/권한/민감 동작). approval_log(업무 narrative)와 구분."""

    __tablename__ = "audit_log"
    id: str = Field(primary_key=True)  # "al-xxxx"
    user_id: Optional[str] = Field(default=None, foreign_key="person.id", index=True)
    action: str  # login|logout|export|scope.denied|prompt_injection.suspected ...
    target: Optional[str] = None
    ip: Optional[str] = None
    detail: Optional[str] = None
    created_at: datetime = Field(default_factory=now, index=True)


# ---------- 설정(런타임 사용자 환경설정 오버레이) ----------
class AppSetting(SQLModel, table=True):
    """key/value 사용자 설정. .env(읽기전용 ops 구성)와 별개의 런타임 오버레이.

    비어 있으면(기본) 앱 동작은 .env/Settings 와 100% 동일하다.
    현재 소비처는 LLM 연결(llm.provider|model|host|timeout) — runtime_config 오버레이가 읽는다.
    """

    __tablename__ = "app_setting"
    key: str = Field(primary_key=True)  # 예: "llm.provider"
    value: str = ""  # 항상 문자열 직렬화(숫자/불리언도 str)
    updated_at: datetime = Field(default_factory=now)
