import { mkdir, appendFile, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { validateEvent, TelemetryEvent } from './telemetryEvents.js';
import type { AiFailedReason } from './telemetryEvents.js';
import { aggregateStats } from './telemetryStats.js';
import { kstTodayIso, DAY_MS } from '../../shared/util/kstDate.js';

export interface TelemetryServiceOptions {
  silent?: boolean;
}

export type EmitInput =
  | { kind: 'capture'; payload: { noteId: string; rawTextLength: number; hasMedia: boolean } }
  | { kind: 'ai_succeeded'; payload: { noteId: string; durationMs: number; attempts: number } }
  | { kind: 'ai_failed'; payload: { noteId: string; reason: AiFailedReason; attempts: number } }
  | { 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: 'ollama_unreachable'; payload: { reason: string } }
  | { kind: 'ollama_recovered'; payload: { downtimeMs: number } }
  | { kind: 'ollama_recheck_manual'; payload: Record<string, never> }
  | { kind: 'ai_retry_manual'; payload: { failedCount: number } }
  | { kind: 'tag_vocab_hit'; payload: { tagId: number; vocabSize: number } }
  | { kind: 'tag_vocab_miss'; payload: { vocabSize: number } }
  | { kind: 'recall_shown'; payload: { noteId: string; ageDays: number } }
  | { kind: 'recall_opened'; payload: { noteId: string } }
  | { kind: 'recall_dismissed'; payload: { noteId: string } }
  | { kind: 'recall_snoozed'; payload: { noteId: string } };

export class TelemetryService {
  constructor(
    private dir: string,
    private now: () => Date = () => new Date(),
    private retentionDays: number = 14,
    private opts: TelemetryServiceOptions = {}
  ) {}

  async cleanupOldFiles(): Promise<{ removed: string[] }> {
    const removed: string[] = [];
    let entries: string[];
    try {
      entries = await readdir(this.dir);
    } catch {
      return { removed };
    }
    const cutoff = new Date(this.now().getTime() - this.retentionDays * DAY_MS);
    const cutoffIso = kstTodayIso(cutoff);  // KST 일자 비교
    for (const name of entries) {
      const m = /^events-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name);
      if (!m) continue;
      const fileDate = m[1]!;
      if (fileDate < cutoffIso) {
        try {
          await unlink(join(this.dir, name));
          removed.push(name);
        } catch {
          // ignore — best-effort cleanup
        }
      }
    }
    return { removed };
  }

  async emit(input: EmitInput): Promise<void> {
    // 회차 1 review (PR #13) — `now()` 한 번만 호출. KST 자정 경계에서 ts 와 파일명 일자가
    // 어긋나는 것을 방지.
    const nowDate = this.now();
    const ts = nowDate.toISOString();
    const event = validateEvent({ ts, kind: input.kind, payload: input.payload });
    const filePath = join(this.dir, `events-${kstTodayIso(nowDate)}.jsonl`);
    try {
      await mkdir(this.dir, { recursive: true });
      await appendFile(filePath, JSON.stringify(event) + '\n', 'utf8');
    } catch (err) {
      if (this.opts.silent) return;
      throw err;
    }
  }

  async readAllRecent(): Promise<TelemetryEvent[]> {
    const events: TelemetryEvent[] = [];
    let entries: string[];
    try {
      entries = await readdir(this.dir);
    } catch {
      return events;
    }
    const cutoffMs = this.now().getTime() - this.retentionDays * DAY_MS;
    const cutoffIso = kstTodayIso(new Date(cutoffMs));
    // 회차 1 review (PR #13) — 매직 슬라이스 `n.slice(7, 17)` 대신 정규식 capture 그룹으로
    // 일자를 추출. prefix 변경 시 정규식 한 곳만 고치면 됨.
    const datePattern = /^events-(\d{4}-\d{2}-\d{2})\.jsonl$/;
    const fileNames = entries
      .filter((n) => {
        const m = datePattern.exec(n);
        return m !== null && m[1]! >= cutoffIso;
      })
      .sort();
    for (const name of fileNames) {
      let raw: string;
      try {
        raw = await readFile(join(this.dir, name), 'utf8');
      } catch {
        continue;
      }
      for (const line of raw.split('\n')) {
        const trimmed = line.trim();
        if (trimmed.length === 0) continue;
        let parsed: unknown;
        try {
          parsed = JSON.parse(trimmed);
        } catch {
          continue;
        }
        try {
          events.push(validateEvent(parsed));
        } catch {
          continue;
        }
      }
    }
    return events;
  }

  async exportTo(outDir: string): Promise<{ eventCount: number }> {
    const events = await this.readAllRecent();
    await mkdir(outDir, { recursive: true });
    const eventsContent = events.map((e) => JSON.stringify(e)).join('\n') + (events.length > 0 ? '\n' : '');
    await writeFile(join(outDir, 'events.jsonl'), eventsContent, 'utf8');
    const stats = aggregateStats(events, this.now());
    await writeFile(join(outDir, 'stats.md'), stats.md, 'utf8');
    return { eventCount: stats.eventCount };
  }
}
