import { nanoid } from "nanoid";

import type {
  PerformanceSnapshot,
  PublicationRecord,
  ScoringRun,
} from "../calibration/types";
import type {
  CaseInput,
  OptimizationCase,
  OptimizationCaseStatus,
  OptimizationCaseType,
  OptimizationResultVersion,
  ResultVersionStatus,
} from "../cases/types";
import type {
  OptimizationFactCard,
  ImageInput,
  OptimizedArticle,
  PublishPlatform,
  QaReport,
} from "../domain/types";
import type { AppRepository } from "./repository";
import type { ArticleJob, BrandTemplate } from "./repositories";

type JsonObject = Record<string, unknown>;

interface BrandTemplateRow {
  id: string;
  brand_name: string;
  company_full_name: string;
  company_short_names: string;
  product_names: string;
  target_industries: string;
  target_audience: string;
  verified_claims: string;
  forbidden_claims: string;
  tone_rules: string;
  created_at: string;
  updated_at: string;
}

interface ArticleJobRow {
  id: string;
  brand_template_id: string | null;
  case_id: string | null;
  source_title: string;
  source_body: string;
  image_inputs: string;
  publish_platform: PublishPlatform;
  user_instructions: string;
  status: string;
  export_paths: string;
  created_at: string;
  updated_at: string;
}

interface FactCardRow {
  job_id: string;
  fact_card: string;
}

interface OptimizedArticleRow {
  job_id: string;
  revision: number;
  article: string;
}

interface QaReportRow {
  job_id: string;
  revision: number;
  report: string;
}

interface ScoringRunRow {
  id: string;
  result_version_id: string | null;
  case_type: OptimizationCaseType;
  job_id: string | null;
  revision: number | null;
  rubric_version_id: string;
  dimension_scores: string;
  composite_score: number;
  rationale: string;
  created_at: string;
}

interface PublicationRecordRow {
  id: string;
  result_version_id: string | null;
  job_id: string | null;
  revision: number | null;
  publish_target: string;
  url: string;
  published_at: string;
  status: "draft" | "published" | "archived";
  notes: string;
  created_at: string;
  updated_at: string;
}

interface PerformanceSnapshotRow {
  id: string;
  publication_id: string;
  source: "manual" | `adapter:${string}`;
  window_label: string;
  metrics: string;
  feedback_summary: string;
  raw_reference: string | null;
  snapshot_at: string;
}

interface OptimizationCaseRow {
  id: string;
  case_type: OptimizationCaseType;
  title: string;
  summary: string;
  status: OptimizationCaseStatus;
  customer_name: string;
  brand_name: string;
  project_tags: string;
  notes: string;
  publish_target: string;
  source_excerpt: string;
  result_excerpt: string;
  latest_result_version_id: string | null;
  latest_version_number: number | null;
  last_error_stage: string | null;
  last_error_summary: string | null;
  archived_at: string | null;
  created_at: string;
  updated_at: string;
}

interface CaseInputRow {
  case_id: string;
  case_type: OptimizationCaseType;
  article_job_id: string | null;
  payload: string;
  created_at: string;
  updated_at: string;
}

interface OptimizationResultVersionRow {
  id: string;
  case_id: string;
  case_type: OptimizationCaseType;
  version: number;
  status: ResultVersionStatus;
  article_job_id: string | null;
  article_revision: number | null;
  result_summary: string;
  payload: string;
  process_summary: string;
  llm_audit_summary: string;
  error_stage: string | null;
  error_summary: string | null;
  created_at: string;
}

function nowIso() {
  return new Date().toISOString();
}

function serialize(value: unknown) {
  return JSON.stringify(value);
}

function parseJson<T>(value: string): T {
  return JSON.parse(value) as T;
}

function toBrandTemplate(row: BrandTemplateRow): BrandTemplate {
  return {
    ...row,
    company_short_names: parseJson<string[]>(row.company_short_names),
    product_names: parseJson<string[]>(row.product_names),
    target_industries: parseJson<string[]>(row.target_industries),
    target_audience: parseJson<string[]>(row.target_audience),
    verified_claims: parseJson<string[]>(row.verified_claims),
    forbidden_claims: parseJson<string[]>(row.forbidden_claims),
    tone_rules: parseJson<JsonObject>(row.tone_rules),
  };
}

function toArticleJob(row: ArticleJobRow): ArticleJob {
  return {
    ...row,
    image_inputs: parseJson<ImageInput[]>(row.image_inputs),
    export_paths: parseJson<Record<string, string>>(row.export_paths),
  };
}

function toScoringRun(row: ScoringRunRow): ScoringRun {
  return {
    ...row,
    dimension_scores: parseJson<Record<string, number>>(row.dimension_scores),
  };
}

function toPublicationRecord(row: PublicationRecordRow): PublicationRecord {
  return {
    ...row,
    platform: isPublishPlatform(row.publish_target)
      ? row.publish_target
      : undefined,
  };
}

function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot {
  return {
    ...row,
    raw_reference: row.raw_reference ?? undefined,
    metrics: parseJson<PerformanceSnapshot["metrics"]>(row.metrics),
  };
}

function toOptimizationCase(row: OptimizationCaseRow): OptimizationCase {
  return {
    ...row,
    project_tags: parseJson<string[]>(row.project_tags),
  };
}

function toCaseInput(row: CaseInputRow): CaseInput {
  return {
    ...row,
    payload: parseJson<CaseInput["payload"]>(row.payload),
  };
}

function toOptimizationResultVersion(
  row: OptimizationResultVersionRow,
): OptimizationResultVersion {
  return {
    ...row,
    payload: row.payload
      ? parseJson<OptimizationResultVersion["payload"]>(row.payload)
      : null,
    process_summary: parseJson<OptimizationResultVersion["process_summary"]>(
      row.process_summary,
    ),
    llm_audit_summary: parseJson<OptimizationResultVersion["llm_audit_summary"]>(
      row.llm_audit_summary,
    ),
  };
}

function isPublishPlatform(value: string): value is PublishPlatform {
  return [
    "official_site",
    "media_article",
    "comparison_review",
    "recommendation_list",
  ].includes(value);
}

async function updateD1CaseArchiveState(
  db: D1Database,
  caseId: string,
  archived: boolean,
) {
  const existing = await db
    .prepare("select * from optimization_cases where id = ?")
    .bind(caseId)
    .first<OptimizationCaseRow>();
  if (!existing) return null;

  const timestamp = nowIso();
  const restoredStatus = existing.latest_result_version_id
    ? "optimized"
    : existing.last_error_summary
      ? "failed"
      : "running";
  await db
    .prepare(
      `update optimization_cases set
        status = ?,
        archived_at = ?,
        updated_at = ?
       where id = ?`,
    )
    .bind(
      archived ? "archived" : restoredStatus,
      archived ? timestamp : null,
      timestamp,
      caseId,
    )
    .run();

  const row = await db
    .prepare("select * from optimization_cases where id = ?")
    .bind(caseId)
    .first<OptimizationCaseRow>();
  return row ? toOptimizationCase(row) : null;
}

export function createD1Repository(db: D1Database): AppRepository {
  return {
    async createBrandTemplate(input) {
      const createdAt = nowIso();
      const template: BrandTemplate = {
        id: `brand_${nanoid(10)}`,
        ...input,
        created_at: createdAt,
        updated_at: createdAt,
      };

      await db
        .prepare(
          `insert into brand_templates (
            id, brand_name, company_full_name, company_short_names, product_names,
            target_industries, target_audience, verified_claims, forbidden_claims,
            tone_rules, created_at, updated_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          template.id,
          template.brand_name,
          template.company_full_name,
          serialize(template.company_short_names),
          serialize(template.product_names),
          serialize(template.target_industries),
          serialize(template.target_audience),
          serialize(template.verified_claims),
          serialize(template.forbidden_claims),
          serialize(template.tone_rules),
          template.created_at,
          template.updated_at,
        )
        .run();

      return template;
    },
    async listBrandTemplates() {
      const result = await db
        .prepare("select * from brand_templates order by updated_at desc")
        .all<BrandTemplateRow>();
      return result.results.map(toBrandTemplate);
    },
    async getBrandTemplate(id) {
      const row = await db
        .prepare("select * from brand_templates where id = ?")
        .bind(id)
        .first<BrandTemplateRow>();
      return row ? toBrandTemplate(row) : null;
    },
    async createArticleJob(input) {
      const createdAt = nowIso();
      const job: ArticleJob = {
        id: `job_${nanoid(10)}`,
        brand_template_id: input.brand_template_id ?? null,
        case_id: input.case_id ?? null,
        source_title: input.source_title,
        source_body: input.source_body,
        image_inputs: input.image_inputs,
        publish_platform: input.publish_platform,
        user_instructions: input.user_instructions,
        status: "draft",
        export_paths: {},
        created_at: createdAt,
        updated_at: createdAt,
      };

      await db
        .prepare(
          `insert into article_jobs (
            id, brand_template_id, case_id, source_title, source_body, image_inputs,
            publish_platform, user_instructions, status, export_paths, created_at, updated_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          job.id,
          job.brand_template_id,
          job.case_id,
          job.source_title,
          job.source_body,
          serialize(job.image_inputs),
          job.publish_platform,
          job.user_instructions,
          job.status,
          serialize(job.export_paths),
          job.created_at,
          job.updated_at,
        )
        .run();

      return job;
    },
    async getArticleJob(id) {
      const row = await db
        .prepare("select * from article_jobs where id = ?")
        .bind(id)
        .first<ArticleJobRow>();
      return row ? toArticleJob(row) : null;
    },
    async updateArticleJob(id, changes) {
      const existing = await this.getArticleJob(id);
      if (!existing) return null;
      const updated = {
        brand_template_id: changes.brand_template_id ?? existing.brand_template_id,
        status: changes.status ?? existing.status,
        export_paths: changes.export_paths ?? existing.export_paths,
        updated_at: nowIso(),
      };
      await db
        .prepare(
          `update article_jobs set
            brand_template_id = ?,
            status = ?,
            export_paths = ?,
            updated_at = ?
           where id = ?`,
        )
        .bind(
          updated.brand_template_id,
          updated.status,
          serialize(updated.export_paths),
          updated.updated_at,
          id,
        )
        .run();
      return this.getArticleJob(id);
    },
    async saveFactCard(jobId, factCard) {
      const timestamp = nowIso();
      await db
        .prepare(
          `insert into fact_cards (
            job_id, source, fact_card, confirmed_by_user, created_at, updated_at
          ) values (?, ?, ?, ?, ?, ?)
          on conflict(job_id) do update set
            fact_card = excluded.fact_card,
            confirmed_by_user = excluded.confirmed_by_user,
            updated_at = excluded.updated_at`,
        )
        .bind(
          jobId,
          factCard.confirmed_by_user
            ? "auto_extract_then_user_confirmed"
            : "auto_extract_for_optimization",
          serialize(factCard),
          factCard.confirmed_by_user ? 1 : 0,
          timestamp,
          timestamp,
        )
        .run();
      return { job_id: jobId, ...factCard };
    },
    async getFactCard(jobId) {
      const row = await db
        .prepare("select job_id, fact_card from fact_cards where job_id = ?")
        .bind(jobId)
        .first<FactCardRow>();
      return row
        ? { job_id: row.job_id, ...parseJson<OptimizationFactCard>(row.fact_card) }
        : null;
    },
    async saveOptimizedArticle(jobId, article) {
      const latest = await this.getLatestOptimizedArticle(jobId);
      const revision = (latest?.revision ?? 0) + 1;
      const saved = { ...article, job_id: jobId, revision };
      await db
        .prepare(
          `insert into optimized_articles (job_id, revision, article, created_at)
           values (?, ?, ?, ?)`,
        )
        .bind(jobId, revision, serialize(saved), nowIso())
        .run();
      return saved;
    },
    async getLatestOptimizedArticle(jobId) {
      const row = await db
        .prepare(
          `select job_id, revision, article
           from optimized_articles
           where job_id = ?
           order by revision desc
           limit 1`,
        )
        .bind(jobId)
        .first<OptimizedArticleRow>();
      return row ? parseJson<OptimizedArticle>(row.article) : null;
    },
    async saveQaReport(jobId, revision, report) {
      const saved = { ...report, job_id: jobId, revision };
      await db
        .prepare(
          `insert into qa_reports (job_id, revision, report, created_at)
           values (?, ?, ?, ?)
           on conflict(job_id, revision) do update set report = excluded.report`,
        )
        .bind(jobId, revision, serialize(saved), nowIso())
        .run();
      return saved;
    },
    async getLatestQaReport(jobId) {
      const row = await db
        .prepare(
          `select job_id, revision, report
           from qa_reports
           where job_id = ?
           order by revision desc
           limit 1`,
        )
        .bind(jobId)
        .first<QaReportRow>();
      return row ? parseJson<QaReport>(row.report) : null;
    },
    async createOptimizationCase(input) {
      const timestamp = nowIso();
      const optimizationCase: OptimizationCase = {
        id: `case_${nanoid(10)}`,
        case_type: input.case_type,
        title: input.title,
        summary: input.summary,
        status: "running",
        customer_name: "",
        brand_name: "",
        project_tags: [],
        notes: "",
        publish_target: input.publish_target,
        source_excerpt: input.source_excerpt,
        result_excerpt: "",
        latest_result_version_id: null,
        latest_version_number: null,
        last_error_stage: null,
        last_error_summary: null,
        archived_at: null,
        created_at: timestamp,
        updated_at: timestamp,
      };
      await db
        .prepare(
          `insert into optimization_cases (
            id, case_type, title, summary, status, customer_name, brand_name,
            project_tags, notes, publish_target, source_excerpt, result_excerpt,
            latest_result_version_id, latest_version_number, last_error_stage,
            last_error_summary, archived_at, created_at, updated_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          optimizationCase.id,
          optimizationCase.case_type,
          optimizationCase.title,
          optimizationCase.summary,
          optimizationCase.status,
          optimizationCase.customer_name,
          optimizationCase.brand_name,
          serialize(optimizationCase.project_tags),
          optimizationCase.notes,
          optimizationCase.publish_target,
          optimizationCase.source_excerpt,
          optimizationCase.result_excerpt,
          optimizationCase.latest_result_version_id,
          optimizationCase.latest_version_number,
          optimizationCase.last_error_stage,
          optimizationCase.last_error_summary,
          optimizationCase.archived_at,
          optimizationCase.created_at,
          optimizationCase.updated_at,
        )
        .run();
      return optimizationCase;
    },
    async saveCaseInput(input) {
      const timestamp = nowIso();
      const saved: CaseInput = {
        ...input,
        created_at: timestamp,
        updated_at: timestamp,
      };
      await db
        .prepare(
          `insert into case_inputs (
            case_id, case_type, article_job_id, payload, created_at, updated_at
          ) values (?, ?, ?, ?, ?, ?)
          on conflict(case_id) do update set
            case_type = excluded.case_type,
            article_job_id = excluded.article_job_id,
            payload = excluded.payload,
            updated_at = excluded.updated_at`,
        )
        .bind(
          saved.case_id,
          saved.case_type,
          saved.article_job_id,
          serialize(saved.payload),
          saved.created_at,
          saved.updated_at,
        )
        .run();
      return saved;
    },
    async listOptimizationCases(filters) {
      const where: string[] = [];
      const values: unknown[] = [];
      if (!filters.include_archived) where.push("archived_at is null");
      if (filters.case_type) {
        where.push("case_type = ?");
        values.push(filters.case_type);
      }
      if (filters.status) {
        where.push("status = ?");
        values.push(filters.status);
      }
      if (filters.publish_target) {
        where.push("publish_target = ?");
        values.push(filters.publish_target);
      }
      if (filters.project_tag) {
        where.push("project_tags like ?");
        values.push(`%"${filters.project_tag}"%`);
      }
      if (filters.created_from) {
        where.push("created_at >= ?");
        values.push(filters.created_from);
      }
      if (filters.created_to) {
        where.push("created_at <= ?");
        values.push(filters.created_to);
      }
      if (filters.q) {
        where.push(
          "(title like ? or summary like ? or source_excerpt like ? or result_excerpt like ? or customer_name like ? or brand_name like ? or notes like ?)",
        );
        const keyword = `%${filters.q}%`;
        values.push(keyword, keyword, keyword, keyword, keyword, keyword, keyword);
      }
      const clause = where.length > 0 ? `where ${where.join(" and ")}` : "";
      const prepared = db.prepare(
        `select * from optimization_cases ${clause} order by updated_at desc`,
      );
      const result =
        values.length > 0
          ? await prepared.bind(...values).all<OptimizationCaseRow>()
          : await prepared.all<OptimizationCaseRow>();
      return result.results.map(toOptimizationCase);
    },
    async getOptimizationCaseDetail(caseId) {
      const caseRow = await db
        .prepare("select * from optimization_cases where id = ?")
        .bind(caseId)
        .first<OptimizationCaseRow>();
      if (!caseRow) return null;
      const inputRow = await db
        .prepare("select * from case_inputs where case_id = ?")
        .bind(caseId)
        .first<CaseInputRow>();
      const versionRows = await db
        .prepare(
          `select * from optimization_result_versions
           where case_id = ?
           order by version desc`,
        )
        .bind(caseId)
        .all<OptimizationResultVersionRow>();
      return {
        case: toOptimizationCase(caseRow),
        input: inputRow ? toCaseInput(inputRow) : null,
        versions: versionRows.results.map(toOptimizationResultVersion),
      };
    },
    async updateOptimizationCaseMetadata(caseId, changes) {
      const existing = await db
        .prepare("select * from optimization_cases where id = ?")
        .bind(caseId)
        .first<OptimizationCaseRow>();
      if (!existing) return null;
      await db
        .prepare(
          `update optimization_cases set
            title = ?,
            customer_name = ?,
            brand_name = ?,
            project_tags = ?,
            notes = ?,
            updated_at = ?
           where id = ?`,
        )
        .bind(
          changes.title ?? existing.title,
          changes.customer_name ?? existing.customer_name,
          changes.brand_name ?? existing.brand_name,
          changes.project_tags === undefined
            ? existing.project_tags
            : serialize(changes.project_tags),
          changes.notes ?? existing.notes,
          nowIso(),
          caseId,
        )
        .run();
      const row = await db
        .prepare("select * from optimization_cases where id = ?")
        .bind(caseId)
        .first<OptimizationCaseRow>();
      return row ? toOptimizationCase(row) : null;
    },
    async archiveOptimizationCase(caseId) {
      return updateD1CaseArchiveState(db, caseId, true);
    },
    async restoreOptimizationCase(caseId) {
      return updateD1CaseArchiveState(db, caseId, false);
    },
    async markOptimizationCaseFailed(caseId, input) {
      await db
        .prepare(
          `update optimization_cases set
            status = 'failed',
            last_error_stage = ?,
            last_error_summary = ?,
            updated_at = ?
           where id = ?`,
        )
        .bind(input.error_stage, input.error_summary, nowIso(), caseId)
        .run();
      const row = await db
        .prepare("select * from optimization_cases where id = ?")
        .bind(caseId)
        .first<OptimizationCaseRow>();
      return row ? toOptimizationCase(row) : null;
    },
    async createOptimizationResultVersion(input) {
      const latest = await db
        .prepare(
          "select max(version) as version from optimization_result_versions where case_id = ?",
        )
        .bind(input.case_id)
        .first<{ version: number | null }>();
      const createdAt = nowIso();
      const resultVersion: OptimizationResultVersion = {
        id: `ver_${nanoid(10)}`,
        ...input,
        version: (latest?.version ?? 0) + 1,
        created_at: createdAt,
      };
      await db
        .prepare(
          `insert into optimization_result_versions (
            id, case_id, case_type, version, status, article_job_id, article_revision,
            result_summary, payload, process_summary, llm_audit_summary,
            error_stage, error_summary, created_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          resultVersion.id,
          resultVersion.case_id,
          resultVersion.case_type,
          resultVersion.version,
          resultVersion.status,
          resultVersion.article_job_id,
          resultVersion.article_revision,
          resultVersion.result_summary,
          resultVersion.payload ? serialize(resultVersion.payload) : "",
          serialize(resultVersion.process_summary),
          serialize(resultVersion.llm_audit_summary),
          resultVersion.error_stage,
          resultVersion.error_summary,
          resultVersion.created_at,
        )
        .run();
      await db
        .prepare(
          `update optimization_cases set
            status = ?,
            result_excerpt = ?,
            latest_result_version_id = ?,
            latest_version_number = ?,
            last_error_stage = ?,
            last_error_summary = ?,
            updated_at = ?
           where id = ?`,
        )
        .bind(
          input.status === "optimized" ? "optimized" : "failed",
          input.result_summary,
          resultVersion.id,
          resultVersion.version,
          input.error_stage,
          input.error_summary,
          createdAt,
          input.case_id,
        )
        .run();
      return resultVersion;
    },
    async getOptimizationResultVersion(versionId) {
      const row = await db
        .prepare("select * from optimization_result_versions where id = ?")
        .bind(versionId)
        .first<OptimizationResultVersionRow>();
      return row ? toOptimizationResultVersion(row) : null;
    },
    async findResultVersionForArticleRevision(jobId, revision) {
      const row = await db
        .prepare(
          `select * from optimization_result_versions
           where article_job_id = ? and article_revision = ?
           order by version desc
           limit 1`,
        )
        .bind(jobId, revision)
        .first<OptimizationResultVersionRow>();
      return row ? toOptimizationResultVersion(row) : null;
    },
    async saveRubricVersion(rubric) {
      await db
        .prepare(
          `insert into rubric_versions (
            id, version, name, dimensions, formula, is_active, created_at
          ) values (?, ?, ?, ?, ?, ?, ?)
          on conflict(id) do update set
            version = excluded.version,
            name = excluded.name,
            dimensions = excluded.dimensions,
            formula = excluded.formula,
            is_active = excluded.is_active`,
        )
        .bind(
          rubric.id,
          rubric.version,
          rubric.name,
          serialize(rubric.dimensions),
          rubric.formula,
          rubric.is_active ? 1 : 0,
          rubric.created_at,
        )
        .run();
      return rubric;
    },
    async saveScoringRun(run) {
      await db
        .prepare(
          `insert into scoring_runs (
            id, result_version_id, case_type, job_id, revision, rubric_version_id, dimension_scores,
            composite_score, rationale, created_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          run.id,
          run.result_version_id ?? null,
          run.case_type ?? "article",
          run.job_id,
          run.revision,
          run.rubric_version_id,
          serialize(run.dimension_scores),
          run.composite_score,
          run.rationale,
          run.created_at,
        )
        .run();
      return run;
    },
    async getLatestScoringRun(jobId, revision) {
      const row = await db
        .prepare(
          `select * from scoring_runs
           where job_id = ? and revision = ?
           order by created_at desc
           limit 1`,
        )
        .bind(jobId, revision)
        .first<ScoringRunRow>();
      return row ? toScoringRun(row) : null;
    },
    async getLatestScoringRunForResultVersion(resultVersionId) {
      const row = await db
        .prepare(
          `select * from scoring_runs
           where result_version_id = ?
           order by created_at desc
           limit 1`,
        )
        .bind(resultVersionId)
        .first<ScoringRunRow>();
      return row ? toScoringRun(row) : null;
    },
    async createPublicationRecord(input) {
      const timestamp = nowIso();
      const publishTarget = input.publish_target ?? input.platform ?? "未指定";
      const record: PublicationRecord = {
        id: `pub_${nanoid(10)}`,
        ...input,
        result_version_id: input.result_version_id ?? null,
        publish_target: publishTarget,
        platform: isPublishPlatform(publishTarget) ? publishTarget : input.platform,
        created_at: timestamp,
        updated_at: timestamp,
      };
      await db
        .prepare(
          `insert into publication_records (
            id, result_version_id, job_id, revision, publish_target, url, published_at, status,
            notes, created_at, updated_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          record.id,
          record.result_version_id,
          record.job_id,
          record.revision,
          record.publish_target,
          record.url,
          record.published_at,
          record.status,
          record.notes,
          record.created_at,
          record.updated_at,
        )
        .run();
      return record;
    },
    async listPublicationRecords(jobId) {
      const result = await db
        .prepare(
          "select * from publication_records where job_id = ? order by published_at desc",
        )
        .bind(jobId)
        .all<PublicationRecordRow>();
      return result.results.map(toPublicationRecord);
    },
    async listPublicationRecordsForResultVersion(resultVersionId) {
      const result = await db
        .prepare(
          "select * from publication_records where result_version_id = ? order by published_at desc",
        )
        .bind(resultVersionId)
        .all<PublicationRecordRow>();
      return result.results.map(toPublicationRecord);
    },
    async getPublicationRecord(id) {
      const row = await db
        .prepare("select * from publication_records where id = ?")
        .bind(id)
        .first<PublicationRecordRow>();
      return row ? toPublicationRecord(row) : null;
    },
    async savePerformanceSnapshot(snapshot) {
      await db
        .prepare(
          `insert into performance_snapshots (
            id, publication_id, source, window_label, metrics,
            feedback_summary, raw_reference, snapshot_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          snapshot.id,
          snapshot.publication_id,
          snapshot.source,
          snapshot.window_label,
          serialize(snapshot.metrics),
          snapshot.feedback_summary,
          snapshot.raw_reference ?? null,
          snapshot.snapshot_at,
        )
        .run();
      return snapshot;
    },
    async listPerformanceSnapshots(publicationId) {
      const result = await db
        .prepare(
          "select * from performance_snapshots where publication_id = ? order by snapshot_at desc",
        )
        .bind(publicationId)
        .all<PerformanceSnapshotRow>();
      return result.results.map(toPerformanceSnapshot);
    },
    async saveCalibrationEvent(event) {
      await db
        .prepare(
          `insert into calibration_events (
            id, publication_id, scoring_run_id, performance_snapshot_id,
            direction, observations, recommended_action, created_at
          ) values (?, ?, ?, ?, ?, ?, ?, ?)`,
        )
        .bind(
          event.id,
          event.publication_id,
          event.scoring_run_id,
          event.performance_snapshot_id,
          event.direction,
          serialize(event.observations),
          event.recommended_action,
          event.created_at,
        )
        .run();
      return event;
    },
  };
}
