import { create } from 'zustand';
import type { Note, Notebook, PromotionCandidate, ReviewAggregate, WeeklyContinuity } from '@shared/types';
import { inboxApi, notebookApi } from './api.js';
import { nextKstMidnightMs } from '@shared/util/kstDate.js';

function toTitleCase(s: string): string {
  return s.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}

export { selectFilteredNotes } from './selectFilteredNotes.js';

// v0.2.9 Cut B Task 4 — 3탭 view enum + settings.
// v0.4 Task 16 — 'archived' view 제거 (NoteStatus 에서 archived 삭제됨).
// 'inbox' = active, 'completed' = NoteStatus 그대로, 'trash' = trashed (mirror), 'settings' = SettingsPage.
export type InboxView =
  | 'inbox' | 'completed' | 'trash' | 'settings'
  | 'review-daily' | 'review-weekly' | 'review-monthly';

export interface InboxCounts {
  active: number;
  completed: number;
  trashed: number;
}

interface InboxState {
  notes: Note[];
  trashNotes: Note[];
  trashCount: number;
  showTrash: boolean;
  showSettings: boolean;
  // v0.2.9 Cut B Task 4 — view enum + counts. showTrash/showSettings 는 mirror 로 잠시 잔류.
  view: InboxView;
  counts: InboxCounts;
  continuity: WeeklyContinuity;
  pendingCount: number;
  ollamaStatus: { ok: boolean; reason?: string };
  todayCount: number;
  loading: boolean;
  tagFilter: string | null;
  expiredCandidates: Note[];
  expiredSnoozeUntilMs: number | null;
  failedCount: number;
  recallCandidate: Note | null;
  recallSnoozeUntilMs: number | null;
  // v0.2.9 Cut B Task 14 — AI 비활성 모드에서는 OllamaBanner/FailedBanner render skip.
  // 기본 true (기존 사용자 무영향). loadInitial / refreshMeta 가 settings 로드.
  ai_enabled: boolean;
  // v0.2.11 Cut D — FTS5 search + review aggregate state.
  searchQuery: string;
  searchResults: Note[] | null;  // null = 검색 안 한 상태
  reviewData: ReviewAggregate | null;
  // v0.4 — Notebook sidebar state.
  notebooks: Notebook[];
  selectedNotebookId: string | null;
  sidebarVisible: boolean;
  sidebarWidth: number;
  // v0.4 Task 11 — promotion candidates (dismissed/snoozed 필터 적용 후 목록).
  promotionCandidates: PromotionCandidate[];
  loadInitial: () => Promise<void>;
  refreshMeta: () => Promise<void>;
  upsertNote: (note: Note) => void;
  removeNote: (id: string) => void;
  setTagFilter: (tag: string | null) => void;
  setShowSettings: (open: boolean) => void;
  setView: (view: InboxView) => void;
  loadByView: (view: 'inbox' | 'completed' | 'trash') => Promise<void>;
  toggleShowTrash: () => Promise<void>;
  loadTrash: () => Promise<void>;
  restoreNote: (id: string) => Promise<void>;
  permanentDeleteNote: (id: string) => Promise<void>;
  emptyTrash: () => Promise<void>;
  trashExpiredBatch: (ids: string[]) => Promise<void>;
  snoozeExpired: () => void;
  recheckOllama: () => Promise<void>;
  retryAllFailed: () => Promise<void>;
  loadRecallCandidate: () => Promise<void>;
  openRecall: (id: string) => Promise<void>;
  dismissRecallNote: (id: string) => Promise<void>;
  snoozeRecall: () => Promise<void>;
  // v0.2.11 Cut D — search + review actions.
  setSearchQuery: (q: string) => void;
  searchNotes: (q: string) => Promise<void>;
  clearSearch: () => void;
  loadReview: (period: 'daily' | 'weekly' | 'monthly') => Promise<void>;
  // v0.4 — Notebook actions.
  loadNotebooks: () => Promise<void>;
  selectNotebook: (id: string) => void;
  createNotebook: (name: string, color?: string) => Promise<{ ok: boolean; reason?: string }>;
  renameNotebook: (id: string, name: string) => Promise<{ ok: boolean; reason?: string }>;
  setNotebookColor: (id: string, color: string | null) => Promise<void>;
  deleteNotebook: (id: string) => Promise<{ ok: boolean; reason?: string }>;
  moveNoteToNotebook: (noteId: string, notebookId: string) => Promise<void>;
  toggleSidebar: () => void;
  // v0.4 Task 11 — promotion candidate actions.
  loadPromotionCandidates: () => Promise<void>;
  acceptPromotion: (tag: string, customName: string, color: string | undefined) => Promise<void>;
  snoozePromotion: () => Promise<void>;
  dismissPromotion: (tag: string) => Promise<void>;
}

const emptyContinuity: WeeklyContinuity = {
  weekStart: '', weekCount: 0, weekTarget: 7,
  consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null
};

export const useInbox = create<InboxState>((set, get) => ({
  notes: [],
  trashNotes: [],
  trashCount: 0,
  showTrash: false,
  showSettings: false,
  view: 'inbox',
  counts: { active: 0, completed: 0, trashed: 0 },
  continuity: emptyContinuity,
  pendingCount: 0,
  ollamaStatus: { ok: true },
  todayCount: 0,
  loading: false,
  tagFilter: null,
  expiredCandidates: [],
  expiredSnoozeUntilMs: null,
  failedCount: 0,
  recallCandidate: null,
  recallSnoozeUntilMs: null,
  ai_enabled: true,
  searchQuery: '',
  searchResults: null,
  reviewData: null,
  notebooks: [],
  selectedNotebookId: null,
  sidebarVisible: false,
  sidebarWidth: 240,
  promotionCandidates: [],
  async loadInitial() {
    // v0.3.8 — IPC 실패 시 loading=true 영구 stuck 방지. catch 로 reset.
    set({ loading: true });
    try {
      const [notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
        // inbox 탭은 status='active' 만 표시 — loadByView('inbox') 와 동일 path 로 일관성 확보.
        // listNotes 는 deleted_at IS NULL 만 필터 (= active+completed+archived 혼재) 이라 부정확.
        inboxApi.listByStatus('active', { limit: 50 }),
        inboxApi.getContinuity(),
        inboxApi.getPendingCount(),
        inboxApi.getOllamaStatus(),
        inboxApi.getTodayCount(),
        inboxApi.getTrashCount(),
        inboxApi.listExpired(),
        inboxApi.getFailedCount(),
        inboxApi.listRecallCandidate(),
        inboxApi.countsByStatus(),
        inboxApi.getSettings()
      ]);
      set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false });
    } catch (e) {
      // 첫 launch 의 IPC 실패 (DB migration 실패 / main process 비정상) 시 무한 loading 회피.
      // 빈 데이터로 진입하면 사용자가 캡처 시도 → 실제 fail 이 표면화 → 재시도 가능.
      console.error('[inbox] loadInitial failed', e);
      set({ loading: false });
    }
  },
  async refreshMeta() {
    try {
      const [continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
        inboxApi.getContinuity(),
        inboxApi.getPendingCount(),
        inboxApi.getOllamaStatus(),
        inboxApi.getTodayCount(),
        inboxApi.getTrashCount(),
        inboxApi.listExpired(),
        inboxApi.getFailedCount(),
        inboxApi.listRecallCandidate(),
        inboxApi.countsByStatus(),
        inboxApi.getSettings()
      ]);
      set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true });
    } catch (e) {
      // refreshMeta 는 background poll/event 에서 자주 호출 → fail 무시 (다음 호출에 회복).
      console.error('[inbox] refreshMeta failed', e);
    }
  },
  upsertNote(note) {
    // v0.3.8 — status 가 current view 와 매칭될 때만 notes 에 유지. 그 외엔 제거.
    // 이전 구현은 trashed 외 모든 status 를 notes 에 누적 → 사용자가 inbox view 에서
    // 완료/보관 으로 옮긴 노트가 list 에 잔류하는 버그. push-based (setStatus 도 emit) 로
    // 모든 status 전이가 upsertNote 를 거치므로 view-aware filter 가 필수.
    //
    // trashCount/trashNotes 는 server-authoritative. trashNotes 가 cache-loaded
    // (view='trash') 일 때만 trashCount 를 local recompute. 그 외엔 server 값
    // (refreshMeta) 보존. searchResults 도 별도로 갱신 (status 변경 시 list 에서 제거).
    const state = get();
    const view = state.view;
    const showTrash = state.showTrash;
    const viewStatus: 'active' | 'completed' | 'trashed' | null =
      view === 'inbox' ? 'active' :
      view === 'completed' ? 'completed' :
      view === 'trash' ? 'trashed' : null;

    // trashNotes — note.status='trashed' 면 upsert, 아니면 제거.
    const cleanTrash = state.trashNotes.filter((n) => n.id !== note.id);
    let nextTrash = cleanTrash;
    if (note.status === 'trashed') {
      const ti = state.trashNotes.findIndex((n) => n.id === note.id);
      nextTrash = cleanTrash.slice();
      if (ti >= 0) nextTrash.splice(ti, 0, note);
      else nextTrash.unshift(note);
    }

    // notes — current view 의 status 와 매칭되는 경우만 유지/upsert.
    // viewStatus=null (review/settings/검색) 이면 notes 직접 렌더 안 함 → 갱신 skip.
    const cleanNotes = state.notes.filter((n) => n.id !== note.id);
    let nextNotes = state.notes;
    if (viewStatus !== null) {
      if (note.status === viewStatus) {
        const i = state.notes.findIndex((n) => n.id === note.id);
        nextNotes = cleanNotes.slice();
        if (i >= 0) nextNotes.splice(i, 0, note);
        else nextNotes.unshift(note);
      } else {
        nextNotes = cleanNotes;
      }
    }

    // searchResults — null 아니면 동일 패턴으로 갱신 (status 가 current search status 와
    // 안 맞으면 제거, 맞으면 upsert).
    let nextSearch = state.searchResults;
    if (state.searchResults !== null) {
      const cleanSearch = state.searchResults.filter((n) => n.id !== note.id);
      if (viewStatus === null || note.status === viewStatus) {
        // search 가 active 한 view 가 review/settings 면 status filter 없음 → 모두 keep.
        const i = state.searchResults.findIndex((n) => n.id === note.id);
        nextSearch = cleanSearch.slice();
        if (i >= 0) nextSearch.splice(i, 0, note);
        else nextSearch.unshift(note);
      } else {
        nextSearch = cleanSearch;
      }
    }

    set({
      notes: nextNotes,
      trashNotes: nextTrash,
      searchResults: nextSearch,
      ...(showTrash ? { trashCount: nextTrash.length } : {})
    });
  },
  removeNote(id) {
    const cleanNotes = get().notes.filter((n) => n.id !== id);
    const cleanTrash = get().trashNotes.filter((n) => n.id !== id);
    const showTrash = get().showTrash;
    set({
      notes: cleanNotes,
      trashNotes: cleanTrash,
      ...(showTrash ? { trashCount: cleanTrash.length } : {})
    });
  },
  setTagFilter(tag) {
    set({ tagFilter: tag });
  },
  setShowSettings(open) {
    // backward-compat — setView 로 위임. mirror state (view, showTrash, showSettings) 동기 갱신.
    if (open) get().setView('settings');
    else get().setView('inbox');
  },
  setView(view) {
    // view 전환 시 검색/태그 필터 reset — 이전 view 의 필터가 새 view 에 잘못 적용되는 것 방지.
    set({
      view,
      showTrash: view === 'trash',
      showSettings: view === 'settings',
      searchResults: null,
      searchQuery: '',
      tagFilter: null
    });
    // status view 면 해당 status fetch. inbox 도 포함 — 다른 탭에서 돌아올 때 notes 가
    // 이전 status 로 stale 한 상태이므로 재로드 필요.
    if (view === 'inbox' || view === 'completed' || view === 'trash') {
      void get().loadByView(view);
    }
    // v0.2.11 Cut D — review-* view 진입 시 aggregate 로드.
    if (view === 'review-daily') void get().loadReview('daily');
    if (view === 'review-weekly') void get().loadReview('weekly');
    if (view === 'review-monthly') void get().loadReview('monthly');
  },
  async loadByView(view) {
    // v0.3.8 — IPC 실패 시 stale 한 이전 view 의 notes 가 계속 노출되는 사고 방지.
    // fail 시 빈 배열로 reset 해서 사용자에게 "비어있음" 으로 표시 (혼동 < stale).
    const status =
      view === 'trash' ? 'trashed' : view === 'inbox' ? 'active' : view;
    try {
      const notes = await inboxApi.listByStatus(status, { limit: 200 });
      if (view === 'trash') {
        set({ trashNotes: notes, trashCount: notes.length });
      } else {
        set({ notes });
      }
    } catch (e) {
      console.error('[inbox] loadByView failed', view, e);
      if (view === 'trash') set({ trashNotes: [] });
      else set({ notes: [] });
    }
  },
  async toggleShowTrash() {
    const next = !get().showTrash;
    set({ showTrash: next });
    if (next) await get().loadTrash();
  },
  async loadTrash() {
    const trashNotes = await inboxApi.listTrash({ limit: 200 });
    set({ trashNotes, trashCount: trashNotes.length });
  },
  async restoreNote(id) {
    await inboxApi.restoreNote(id);
    // 낙관적 갱신: main 은 trash/restore 시 pushNoteUpdated 를 보내지 않음
    // (현재 AiWorker.onUpdate + setStatus 만 push). 자가 반영이 primary 메커니즘.
    // 전제: 호출 시점에 trashNotes 에 노트가 존재 (T14 trash view 한정 호출).
    // v0.3.8 — status 도 'active' 로 함께 갱신. upsertNote 가 status='trashed' 만 trash 로 라우팅.
    const note = get().trashNotes.find((n) => n.id === id);
    if (note) {
      get().upsertNote({ ...note, deletedAt: null, status: 'active' });
    }
  },
  async permanentDeleteNote(id) {
    const r = await inboxApi.permanentDeleteNote(id);
    if (r.confirmed) get().removeNote(id);
  },
  async emptyTrash() {
    const r = await inboxApi.emptyTrash();
    if (r.confirmed) {
      set({ trashNotes: [], trashCount: 0 });
    }
  },
  async trashExpiredBatch(ids: string[]) {
    const r = await inboxApi.trashExpiredBatch(ids);
    if (!r.confirmed) return;
    const idSet = new Set(ids);
    set({
      expiredCandidates: get().expiredCandidates.filter((n) => !idSet.has(n.id)),
      notes: get().notes.filter((n) => !idSet.has(n.id)),
      trashCount: get().trashCount + r.trashedCount
    });
  },
  snoozeExpired() {
    set({ expiredSnoozeUntilMs: nextKstMidnightMs(Date.now()) });
  },
  async recheckOllama() {
    const status = await inboxApi.ollamaRecheck();
    set({ ollamaStatus: status });
  },
  async retryAllFailed() {
    await inboxApi.retryAllFailed();
    // 낙관적 갱신: failedCount = 0. AiWorker 처리 진행 중에 PendingBanner 가 N건 노출.
    // refreshMeta 가 트리거되면 자연 동기 (worker.onUpdate → main → renderer).
    // 반환된 r.count 는 의도적으로 무시 — 단일 process 환경 (Electron) 이라 race 무관,
    // 모든 ai_status='failed' 가 retry 대상이므로 사용자 시점 카운트는 0 으로 reset 가 정확.
    set({ failedCount: 0 });
  },
  async loadRecallCandidate() {
    const recallCandidate = await inboxApi.listRecallCandidate();
    set({ recallCandidate });
  },
  async openRecall(id) {
    await inboxApi.markRecallOpened(id);
    const recallCandidate = await inboxApi.listRecallCandidate();
    set({ recallCandidate });
  },
  async dismissRecallNote(id) {
    await inboxApi.dismissRecall(id);
    const recallCandidate = await inboxApi.listRecallCandidate();
    // m2 fix — dismiss 후 새 candidate 가 들어와도 이전 snooze 가 적용되지 않도록 clear
    set({ recallCandidate, recallSnoozeUntilMs: null });
  },
  async snoozeRecall() {
    set({ recallSnoozeUntilMs: nextKstMidnightMs(Date.now()) });
    // m1 fix — candidate=null 인 race 케이스 (사용자가 banner 닫힌 직후 클릭) 시
    // snooze 는 적용하되 emit 만 skip. telemetry 누락 받아들임 (의도적).
    const candidate = get().recallCandidate;
    if (candidate) {
      inboxApi.emitRecallSnoozed(candidate.id);
    }
  },
  // v0.2.11 Cut D — FTS5 search + review aggregate actions.
  setSearchQuery(q) {
    set({ searchQuery: q });
    if (q.trim().length === 0) set({ searchResults: null });
  },
  async searchNotes(q) {
    if (q.trim().length === 0) {
      set({ searchResults: null });
      return;
    }
    const view = get().view;
    // 회고/설정 view 일 때는 status filter 무의미 → 그대로 전체 검색
    const status = view === 'completed' || view === 'trash'
      ? (view === 'trash' ? 'trashed' : view)
      : view === 'inbox' ? 'active' : undefined;
    try {
      const r = await inboxApi.search(q, status ? { status } : {});
      set({ searchResults: r });
    } catch (e) {
      // FTS5 query parse error (special char 미escape) / IPC fail → 빈 결과로.
      console.error('[inbox] searchNotes failed', e);
      set({ searchResults: [] });
    }
  },
  clearSearch() {
    set({ searchQuery: '', searchResults: null });
  },
  async loadReview(period) {
    try {
      const data = await inboxApi.reviewAggregate(period);
      set({ reviewData: data });
    } catch (e) {
      // review IPC fail 시 reviewData=null → ReviewView 의 "불러오는 중…" 영구 표시 회피.
      // 빈 aggregate 로 set 해서 사용자에게 "0건" 표기.
      console.error('[inbox] loadReview failed', period, e);
      set({ reviewData: { totalCount: 0, tagCounts: [], dueProgress: { total: 0, passed: 0, pending: 0 }, recentNotes: [] } });
    }
  },
  // v0.4 — Notebook actions.
  async loadNotebooks() {
    const notebooks = await notebookApi.list();
    const current = get().selectedNotebookId;
    // selectedNotebookId 가 null 이면 첫 notebook (가장 오래된 = 기본) 으로 설정.
    const selectedNotebookId = current === null && notebooks.length > 0
      ? notebooks[0]!.id
      : current;
    set({ notebooks, selectedNotebookId });
  },
  selectNotebook(id) {
    set({ selectedNotebookId: id });
  },
  async createNotebook(name, color) {
    const r = await notebookApi.create({ name, color });
    if (r.ok) {
      set({ notebooks: [...get().notebooks, r.notebook] });
      return { ok: true };
    }
    return { ok: false, reason: r.reason };
  },
  async renameNotebook(id, name) {
    const r = await notebookApi.rename(id, name);
    if (r.ok) {
      set({ notebooks: get().notebooks.map((n) => n.id === id ? { ...n, name } : n) });
      return { ok: true };
    }
    return { ok: false, reason: r.reason };
  },
  async setNotebookColor(id, color) {
    await notebookApi.setColor(id, color);
    set({ notebooks: get().notebooks.map((n) => n.id === id ? { ...n, color } : n) });
  },
  async deleteNotebook(id) {
    const r = await notebookApi.delete(id);
    if (r.ok) {
      const remaining = get().notebooks.filter((n) => n.id !== id);
      const wasSelected = get().selectedNotebookId === id;
      set({
        notebooks: remaining,
        selectedNotebookId: wasSelected ? (remaining[0]?.id ?? null) : get().selectedNotebookId
      });
      return { ok: true };
    }
    return { ok: false, reason: r.reason };
  },
  async moveNoteToNotebook(noteId, notebookId) {
    await notebookApi.moveNote(noteId, notebookId);
    await get().refreshMeta();
  },
  toggleSidebar() {
    set({ sidebarVisible: !get().sidebarVisible });
  },
  // v0.4 Task 11 — promotion candidate actions.
  async loadPromotionCandidates() {
    try {
      const [dismissed, snoozeUntil, raw] = await Promise.all([
        inboxApi.getPromotionDismissedTags(),
        inboxApi.getPromotionSnoozeUntil(),
        inboxApi.listPromotionCandidates()
      ]);
      // snoozed_until > now → 빈 배열 (전체 스누즈).
      if (snoozeUntil > Date.now()) {
        set({ promotionCandidates: [] });
        return;
      }
      const dismissedSet = new Set(dismissed);
      const candidates: PromotionCandidate[] = raw
        .filter((c) => !dismissedSet.has(c.tag))
        .map((c) => ({ ...c, suggestedName: toTitleCase(c.tag) }));
      set({ promotionCandidates: candidates });
    } catch (e) {
      console.error('[inbox] loadPromotionCandidates failed', e);
      set({ promotionCandidates: [] });
    }
  },
  async acceptPromotion(tag, customName, color) {
    const candidate = get().promotionCandidates.find((c) => c.tag === tag);
    if (!candidate) return;
    const r = await notebookApi.create({ name: customName, color });
    if (!r.ok) return;
    const notebookId = r.notebook.id;
    await Promise.all(candidate.noteIds.map((noteId) => notebookApi.moveNote(noteId, notebookId)));
    // state: candidate 제거 + notebooks 갱신 + sidebar 열기 + 새 notebook 선택.
    set({
      promotionCandidates: get().promotionCandidates.filter((c) => c.tag !== tag),
      notebooks: [...get().notebooks, r.notebook],
      sidebarVisible: true,
      selectedNotebookId: notebookId
    });
    await get().refreshMeta();
  },
  async snoozePromotion() {
    const snoozeUntil = Date.now() + 24 * 60 * 60 * 1000;
    await inboxApi.setPromotionSnoozeUntil(snoozeUntil);
    set({ promotionCandidates: [] });
  },
  async dismissPromotion(tag) {
    await inboxApi.addPromotionDismissedTag(tag);
    set({ promotionCandidates: get().promotionCandidates.filter((c) => c.tag !== tag) });
  }
}));
