import type { NoteRepository } from '../repository/NoteRepository.js';
import type { MediaStore } from './MediaStore.js';
import type { Note } from '@shared/types';

/**
 * v0.2.9 Cut B — CaptureService 가 ai_enabled 를 조회할 때만 의존하는 좁은 인터페이스.
 * SettingsService 직접 의존을 피해 테스트 mock 이 단순해짐 (entire SettingsService 면 불필요).
 */
export interface AiEnabledSource {
  isAiEnabled(): Promise<boolean>;
}

export interface TelemetryEmitter {
  emit(input:
    | { kind: 'capture'; payload: { noteId: string; rawTextLength: number; hasMedia: boolean } }
    | { kind: 'trash'; payload: { noteId: string } }
    | { kind: 'restore'; payload: { noteId: string } }
    | { kind: 'permanent_delete'; payload: { noteId: string } }
    | { kind: 'empty_trash'; payload: { count: number } }
    | { kind: 'expired_banner_shown'; payload: { candidateCount: number } }
    | { kind: 'expired_batch_trash'; payload: { count: number } }
    | { kind: 'ai_retry_manual'; payload: { failedCount: number } }
    | { kind: 'recall_opened'; payload: { noteId: string } }
    | { kind: 'recall_dismissed'; payload: { noteId: string } }
    | { kind: 'recall_shown'; payload: { noteId: string; ageDays: number } }
    | { kind: 'recall_snoozed'; payload: { noteId: string } }
  ): Promise<void>;
}

export interface CaptureDeps {
  enqueue: (noteId: string) => Promise<void>;
  celebrate: (noteId: string) => void;
  telemetry?: TelemetryEmitter;
  // v0.2.9 Cut B — settings.ai_enabled=false 면 새 노트는 ai_status='disabled' + enqueue skip.
  // 미주입 시 기존 동작 (항상 enabled) 보존 — 기존 caller 무영향.
  settings?: AiEnabledSource;
}

export interface SubmitInput {
  text: string;
  images: ArrayBuffer[];
}

export class CaptureService {
  private lastExpiredShownSig: string | null = null;

  constructor(
    private repo: NoteRepository,
    private store: MediaStore,
    private deps: CaptureDeps
  ) {}

  async submit(input: SubmitInput): Promise<{ noteId: string }> {
    const trimmed = input.text.trim();
    if (trimmed.length === 0 && input.images.length === 0) {
      throw new Error('empty submission');
    }
    // v0.2.9 Cut B — settings 미주입 시 기본 enabled (backward compat).
    const aiEnabled = this.deps.settings ? await this.deps.settings.isAiEnabled() : true;
    const { id } = this.repo.create({
      rawText: input.text,
      aiStatus: aiEnabled ? 'pending' : 'disabled'
    });
    if (input.images.length > 0) {
      const rows = [];
      for (const img of input.images) {
        const buf = Buffer.from(img);
        const saved = await this.store.saveImage(id, buf, 'image/png');
        rows.push({
          noteId: id,
          kind: 'image' as const,
          relPath: saved.relPath,
          mime: saved.mime,
          bytes: saved.bytes
        });
      }
      this.repo.insertMedia(rows);
    }
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'capture',
        payload: {
          noteId: id,
          rawTextLength: input.text.length,
          hasMedia: input.images.length > 0
        }
      }).catch(() => {});
    }
    if (aiEnabled) {
      await this.deps.enqueue(id);
    }
    this.deps.celebrate(id);
    return { noteId: id };
  }

  async deleteNote(noteId: string): Promise<void> {
    // v0.2.3 #4: hard delete → soft delete. media 보존 (restore 시 필요).
    // 이미 trash 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지.
    const note = this.repo.findById(noteId);
    if (!note || note.deletedAt !== null) return;
    this.repo.trash(noteId, new Date().toISOString());
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({ kind: 'trash', payload: { noteId } }).catch(() => {});
    }
  }

  async restoreNote(noteId: string): Promise<void> {
    // 이미 active 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지.
    const before = this.repo.findById(noteId);
    if (!before || before.deletedAt === null) return;
    // v0.2.6 #10 — production path: repo.restoreNote (ai_status reset + pending_jobs 재생성)
    this.repo.restoreNote(noteId);
    // v0.2.6 #10 — in-memory AiWorker queue 갱신: DB 갱신만으로는 다음 앱 실행 시까지 처리 X
    if (before.aiStatus === 'failed' || before.aiStatus === 'pending') {
      await this.deps.enqueue(noteId);
    }
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({ kind: 'restore', payload: { noteId } }).catch(() => {});
    }
  }

  async permanentDeleteNote(noteId: string): Promise<void> {
    // 존재하지 않는 노트는 emit skip — 메트릭 오염 방지.
    const note = this.repo.findById(noteId);
    if (!note) return;
    this.repo.permanentDelete(noteId);
    // best-effort media cleanup — disk 실패해도 telemetry/IPC 흐름은 그대로 (orphan dir
    // 은 future janitor 가 정리). emptyTrash 와 동일 패턴.
    try { await this.store.deleteNoteDirectory(noteId); }
    catch { /* best-effort */ }
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({ kind: 'permanent_delete', payload: { noteId } }).catch(() => {});
    }
  }

  async emptyTrash(): Promise<{ count: number }> {
    const { noteIds } = this.repo.emptyTrash();
    for (const id of noteIds) {
      try { await this.store.deleteNoteDirectory(id); }
      catch { /* best-effort */ }
    }
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({ kind: 'empty_trash', payload: { count: noteIds.length } }).catch(() => {});
    }
    return { count: noteIds.length };
  }

  /**
   * 만료 후보 (due_date < today KST, active, ai_status=done) 조회.
   * candidates 가 비지 않고 signature 가 직전과 다르면 expired_banner_shown 자동 emit.
   * v0.2.3 #5 spec §6.2 — dedup 위치 main 통합.
   */
  async listExpired(now: Date = new Date()): Promise<Note[]> {
    const candidates = this.repo.findExpiredCandidates(now);
    if (candidates.length === 0) {
      // empty → reset sig 으로 의도적: 다시 후보가 차오르면 동일 set 이라도 1회 emit.
      // (사용자가 "오늘 그만" 후 새 만료 노트 들어와도 셀렉션 변화로 재인식)
      this.lastExpiredShownSig = null;
      return candidates;
    }
    const sig = `${candidates.length}:${candidates.slice(0, 3).map((n) => n.id).join('-')}`;
    if (sig !== this.lastExpiredShownSig) {
      this.lastExpiredShownSig = sig;
      if (this.deps.telemetry) {
        await this.deps.telemetry.emit({
          kind: 'expired_banner_shown',
          payload: { candidateCount: candidates.length }
        }).catch(() => {});
      }
    }
    return candidates;
  }

  /**
   * 만료 후보 일괄 trash. 빈 배열은 즉시 no-op.
   * 성공 시 expired_batch_trash 1회 emit (per-id trash emit 은 별도 발화 안 함 —
   * stats.md 에서 `trash` (단건) vs `expired_batch_trash` (배치) 분리 통계).
   */
  async trashExpiredBatch(ids: string[]): Promise<{ trashedCount: number }> {
    if (ids.length === 0) return { trashedCount: 0 };
    const r = this.repo.trashBatch(ids, new Date().toISOString());
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'expired_batch_trash',
        payload: { count: r.trashedCount }
      }).catch(() => {});
    }
    return r;
  }

  /**
   * 모든 ai_status='failed' (active) 노트를 'pending' 으로 reset + worker.enqueue 재투입.
   * 빈 결과는 telemetry emit 안 함 (failedCount ≥ 1 invariant).
   * v0.2.3 #2 retry-all manual trigger.
   */
  async retryAllFailed(): Promise<{ count: number }> {
    const { ids } = this.repo.retryAllFailed(new Date().toISOString());
    for (const id of ids) {
      await this.deps.enqueue(id);
    }
    if (ids.length > 0 && this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'ai_retry_manual',
        payload: { failedCount: ids.length }
      }).catch(() => {});
    }
    return { count: ids.length };
  }

  /** v0.2.3 #6 — 회상 후보 1건 fetch. */
  async listRecallCandidate(): Promise<Note | null> {
    return this.repo.findRecallCandidate();
  }

  /** v0.2.3 #6 — 회상 "열어보기" 시 last_recalled_at 갱신 + recall_opened emit. */
  async markRecallOpened(noteId: string): Promise<{ note: Note }> {
    if (!this.repo.findById(noteId)) throw new Error(`note not found: ${noteId}`);
    this.repo.markRecallOpened(noteId, new Date().toISOString());
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'recall_opened',
        payload: { noteId }
      }).catch(() => {});
    }
    return { note: this.repo.findById(noteId)! };
  }

  /** v0.2.3 #6 — 회상 "더 이상" 시 recall_dismissed_at 갱신 + recall_dismissed emit. */
  async dismissRecall(noteId: string): Promise<{ note: Note }> {
    this.repo.dismissRecall(noteId, new Date().toISOString());
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'recall_dismissed',
        payload: { noteId }
      }).catch(() => {});
    }
    return { note: this.repo.findById(noteId)! };
  }

  /** v0.2.3 #6 — RecallBanner 첫 렌더 시 recall_shown emit (per-note 1회 제약은 renderer 가 보장). */
  async emitRecallShown(noteId: string): Promise<void> {
    const note = this.repo.findById(noteId);
    if (!note) return;
    const ageDays = this.computeAgeDays(note);
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'recall_shown',
        payload: { noteId, ageDays }
      }).catch(() => {});
    }
  }

  /** v0.2.3 #6 — 사용자 "다음에" 클릭 시 recall_snoozed emit. */
  async emitRecallSnoozed(noteId: string): Promise<void> {
    if (this.deps.telemetry) {
      await this.deps.telemetry.emit({
        kind: 'recall_snoozed',
        payload: { noteId }
      }).catch(() => {});
    }
  }

  /** ageDays = (now - max(last_recalled_at, created_at)) / 86_400_000, floor. */
  private computeAgeDays(note: Note): number {
    const ref = note.lastRecalledAt ?? note.createdAt;
    const refMs = new Date(ref).getTime();
    const nowMs = Date.now();
    return Math.max(0, Math.floor((nowMs - refMs) / 86_400_000));
  }
}
