import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createRandomId } from "@best-agent/foundation";
import type {
  ResearchEvidenceCard,
  ResearchReport,
  ResearchRun,
  ResearchSubquestion,
  ThreadId,
  TurnId,
} from "@best-agent/protocol";
import { readJsonLines } from "./json-lines.js";
import type { ThreadStore } from "./thread-store.js";

export interface CreateResearchRunInput {
  objective: string;
  subquestionCount: number;
  threadId: ThreadId;
  turnId: TurnId;
}

export class LocalResearchStore {
  readonly #threads: ThreadStore;

  constructor(threads: ThreadStore) {
    this.#threads = threads;
  }

  async createRun(input: CreateResearchRunInput): Promise<ResearchRun> {
    const run: ResearchRun = {
      createdAt: new Date().toISOString(),
      objective: input.objective,
      runId: createRandomId("research"),
      status: "running",
      subquestionCount: input.subquestionCount,
      threadId: input.threadId,
      turnId: input.turnId,
    };
    this.#ensureRunDirectory(input.threadId, run.runId);
    this.#appendRun(input.threadId, run);
    writeJsonFile(this.#subquestionsPath(input.threadId, run.runId), [] satisfies ResearchSubquestion[]);
    return run;
  }

  async updateRun(threadId: ThreadId, run: ResearchRun): Promise<void> {
    this.#ensureRunDirectory(threadId, run.runId);
    this.#appendRun(threadId, run);
  }

  async writeSubquestions(threadId: ThreadId, runId: string, subquestions: ResearchSubquestion[]): Promise<void> {
    this.#ensureRunDirectory(threadId, runId);
    writeJsonFile(this.#subquestionsPath(threadId, runId), subquestions);
  }

  async appendEvidence(threadId: ThreadId, runId: string, evidence: ResearchEvidenceCard): Promise<void> {
    this.#ensureRunDirectory(threadId, runId);
    appendFileSync(this.#evidencePath(threadId, runId), `${JSON.stringify(evidence)}\n`);
  }

  async writeReport(threadId: ThreadId, runId: string, report: ResearchReport): Promise<void> {
    this.#ensureRunDirectory(threadId, runId);
    writeJsonFile(this.#reportPath(threadId, runId), report);
  }

  async getRun(threadId: ThreadId, runId: string): Promise<ResearchRun | null> {
    const runs = this.#readRuns(threadId);
    return runs.get(runId) ?? null;
  }

  async getLatestRun(threadId: ThreadId): Promise<ResearchRun | null> {
    const runs = Array.from(this.#readRuns(threadId).values()).sort((left, right) => {
      const leftTime = left.completedAt ?? left.createdAt;
      const rightTime = right.completedAt ?? right.createdAt;
      return rightTime.localeCompare(leftTime);
    });
    return runs[0] ?? null;
  }

  async getReport(threadId: ThreadId, runId: string): Promise<ResearchReport | null> {
    return readJsonFile<ResearchReport>(this.#reportPath(threadId, runId));
  }

  async getLatestReport(threadId: ThreadId): Promise<{ report: ResearchReport | null; run: ResearchRun | null }> {
    const run = await this.getLatestRun(threadId);
    if (!run) {
      return { report: null, run: null };
    }
    return {
      report: await this.getReport(threadId, run.runId),
      run,
    };
  }

  async listEvidence(
    threadId: ThreadId,
    runId?: string,
  ): Promise<{ evidence: ResearchEvidenceCard[]; run: ResearchRun | null }> {
    const run = runId ? await this.getRun(threadId, runId) : await this.getLatestRun(threadId);
    if (!run) {
      return { evidence: [], run: null };
    }
    return {
      evidence: readJsonLines<ResearchEvidenceCard>(this.#evidencePath(threadId, run.runId)),
      run,
    };
  }

  #researchDir(threadId: ThreadId): string {
    return join(this.#threads.getThreadPaths(threadId).threadDir, "research");
  }

  #runsPath(threadId: ThreadId): string {
    return join(this.#researchDir(threadId), "runs.jsonl");
  }

  #runDir(threadId: ThreadId, runId: string): string {
    return join(this.#researchDir(threadId), runId);
  }

  #subquestionsPath(threadId: ThreadId, runId: string): string {
    return join(this.#runDir(threadId, runId), "subquestions.json");
  }

  #evidencePath(threadId: ThreadId, runId: string): string {
    return join(this.#runDir(threadId, runId), "evidence.jsonl");
  }

  #reportPath(threadId: ThreadId, runId: string): string {
    return join(this.#runDir(threadId, runId), "report.json");
  }

  #ensureRunDirectory(threadId: ThreadId, runId: string): void {
    mkdirSync(this.#runDir(threadId, runId), { recursive: true });
  }

  #appendRun(threadId: ThreadId, run: ResearchRun): void {
    mkdirSync(this.#researchDir(threadId), { recursive: true });
    appendFileSync(this.#runsPath(threadId), `${JSON.stringify(run)}\n`);
  }

  #readRuns(threadId: ThreadId): Map<string, ResearchRun> {
    const runs = new Map<string, ResearchRun>();
    for (const run of readJsonLines<ResearchRun>(this.#runsPath(threadId))) {
      runs.set(run.runId, run);
    }
    return runs;
  }
}

function readJsonFile<T>(path: string): T | null {
  if (!existsSync(path)) {
    return null;
  }
  return JSON.parse(readFileSync(path, "utf8")) as T;
}

function writeJsonFile(path: string, value: unknown): void {
  writeFileSync(path, JSON.stringify(value, null, 2));
}
