// shortcut: DB 테이블·마이그레이션 없이 파일 저장소(CASE_REPORT_DIR)에 작업을 둔다. 운영 전 제안서 7.1 의 tn_policy_analysis* 테이블로 옮긴다.
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import { join } from 'path';
import { caseReportConfig } from './case-report.config';
import { crError } from './case-report-exception.filter';
import { JobRecord } from './case-report.types';

const ID_RE = /^[0-9a-f-]{36}$/;
const HASH_RE = /^[0-9a-f]{64}$/;
export const JOB_SUBDIRS = [
  'files',
  'extracted',
  'results',
  'sources',
  'work',
  'cache',
  'logs',
];

//* 사용자가 준 값으로 경로를 만들기 전에 반드시 거친다(../ 등 차단). 모양이 틀리면 없는 것과 같게 404
export function assertId(id: string): string {
  if (typeof id !== 'string' || !ID_RE.test(id))
    throw crError(404, 'CR_NOT_FOUND', '찾을 수 없어요.');
  return id;
}

export const isMissing = (e: unknown) =>
  (e as NodeJS.ErrnoException)?.code === 'ENOENT';

@Injectable()
export class CaseReportStore {
  private readonly chains = new Map<string, Promise<unknown>>();

  root(): string {
    return caseReportConfig().dir as string;
  }

  jobDir(jobId: string): string {
    return join(this.root(), 'jobs', assertId(jobId));
  }

  path(jobId: string, ...parts: string[]): string {
    return join(this.jobDir(jobId), ...parts);
  }

  linkPath(tokenHash: string): string {
    if (!HASH_RE.test(tokenHash)) throw new Error('bad link hash');
    return join(this.root(), 'links', `${tokenHash}.json`);
  }

  async ensureRoot(): Promise<void> {
    for (const dir of ['jobs', 'links', 'work'])
      await fs.mkdir(join(this.root(), dir), { recursive: true });
  }

  //* 폴더를 새로 만드는 곳은 여기 하나다 — 그 밖에서는 mkdir -p 로 지워진 작업을 되살리지 않는다
  async createJobDirs(jobId: string): Promise<void> {
    await this.ensureRoot();
    await fs.mkdir(this.jobDir(jobId));
    for (const dir of JOB_SUBDIRS) await fs.mkdir(this.path(jobId, dir));
  }

  async listJobIds(): Promise<string[]> {
    try {
      return (await fs.readdir(join(this.root(), 'jobs'))).filter((n) =>
        ID_RE.test(n),
      );
    } catch (e) {
      if (isMissing(e)) return [];
      throw e;
    }
  }

  async readJson<T>(file: string): Promise<T | null> {
    try {
      return JSON.parse(await fs.readFile(file, 'utf8')) as T;
    } catch (e) {
      if (isMissing(e)) return null;
      throw e;
    }
  }

  //* 임시 파일에 쓰고 rename — 읽는 쪽은 언제나 온전한 파일을 본다. 부모 폴더가 없으면 ENOENT 로 실패한다(되살리지 않음)
  async writeJsonAtomic(file: string, value: unknown): Promise<void> {
    const tmp = `${file}.${randomUUID()}.tmp`;
    await fs.writeFile(tmp, JSON.stringify(value));
    try {
      await fs.rename(tmp, file);
    } catch (e) {
      await fs.rm(tmp, { force: true });
      throw e;
    }
  }

  readJob(jobId: string): Promise<JobRecord | null> {
    return this.readJson<JobRecord>(this.path(jobId, 'job.json'));
  }

  writeJob(job: JobRecord): Promise<void> {
    return this.writeJsonAtomic(this.path(job.jobId, 'job.json'), job);
  }

  //* 작업마다 Promise 사슬 하나 — 같은 작업의 읽기-수정-쓰기는 이 안에서만 한다
  lock<T>(jobId: string, fn: () => Promise<T>): Promise<T> {
    const prev = this.chains.get(jobId) ?? Promise.resolve();
    const run = prev.catch(() => undefined).then(fn);
    const tail = run.catch(() => undefined);
    this.chains.set(jobId, tail);
    void tail.then(() => {
      if (this.chains.get(jobId) === tail) this.chains.delete(jobId);
    });
    return run;
  }

  //* 사슬 안에서 job.json 을 읽고 fn 이 고친 뒤 쓴다. fn 이 던지면 쓰지 않는다
  mutate<T>(jobId: string, fn: (job: JobRecord) => T | Promise<T>): Promise<T> {
    return this.lock(jobId, async () => {
      const job = await this.readJob(jobId);
      if (!job) throw crError(404, 'CR_NOT_FOUND', '찾을 수 없어요.');
      const out = await fn(job);
      job.updatedAt = new Date().toISOString();
      await this.writeJob(job);
      return out;
    });
  }

  //* 워커 시도 하나의 폴더. 작업 폴더가 없으면 null(지워진 작업)
  async makeTaskDir(
    jobId: string | null,
    task: string,
  ): Promise<string | null> {
    const dir = jobId
      ? this.path(jobId, 'work', task)
      : join(this.root(), 'work', task);
    try {
      await fs.rm(dir, { recursive: true, force: true });
      await fs.mkdir(dir);
      await fs.mkdir(join(dir, 'tmp'));
      return dir;
    } catch (e) {
      if (isMissing(e)) return null;
      throw e;
    }
  }
}
