import { randomUUID } from "node:crypto";
import type BetterSqlite3 from "better-sqlite3";

import { auditRetentionMilliseconds } from "./audit-policy.js";

type GenerationStatus = "queued" | "running" | "succeeded" | "failed" | "rejected";

export class PrivateContentError extends Error {
  constructor(readonly code: "notice_required" | "notice_version_conflict" | "not_found") {
    super(code);
    this.name = "PrivateContentError";
  }
}

function iso(value: number) {
  return new Date(value).toISOString();
}

function isGenerationTablePresent(database: BetterSqlite3.Database) {
  return Boolean(database.prepare(
    "SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'generation_jobs'",
  ).get());
}

export class PrivateContentService {
  constructor(
    readonly database: BetterSqlite3.Database,
    readonly currentNoticeVersion: string,
    private readonly clock: () => number = Date.now,
  ) {}

  currentNotice() {
    return {
      version: this.currentNoticeVersion,
      messageKey: "admin.private_content.notice",
    } as const;
  }

  readAcknowledgement(adminUserId: string) {
    const row = this.database.prepare(`
      SELECT private_content_notice_version, private_content_notice_acknowledged_at
      FROM user_profiles WHERE user_id = ?
    `).get(adminUserId) as { private_content_notice_version: string | null; private_content_notice_acknowledged_at: number | null } | undefined;
    return {
      version: row?.private_content_notice_version ?? null,
      acknowledgedAt: row?.private_content_notice_acknowledged_at === null || row?.private_content_notice_acknowledged_at === undefined
        ? null : iso(row.private_content_notice_acknowledged_at),
    };
  }

  isAcknowledged(adminUserId: string) {
    return this.readAcknowledgement(adminUserId).version === this.currentNoticeVersion;
  }

  requireAcknowledgement(adminUserId: string) {
    if (!this.isAcknowledged(adminUserId)) throw new PrivateContentError("notice_required");
  }

  acknowledge(adminUserId: string, expectedNoticeVersion: string) {
    const now = this.clock();
    return this.database.transaction(() => {
      if (expectedNoticeVersion !== this.currentNoticeVersion) {
        throw new PrivateContentError("notice_version_conflict");
      }
      this.database.prepare(`
        INSERT INTO user_profiles (
          user_id, creator_name, social_id, private_content_notice_version,
          private_content_notice_acknowledged_at
        ) VALUES (?, '', '', ?, ?)
        ON CONFLICT(user_id) DO UPDATE SET
          private_content_notice_version = excluded.private_content_notice_version,
          private_content_notice_acknowledged_at =
            CASE WHEN user_profiles.private_content_notice_version = excluded.private_content_notice_version
              THEN user_profiles.private_content_notice_acknowledged_at ELSE excluded.private_content_notice_acknowledged_at END
      `).run(adminUserId, this.currentNoticeVersion, now);
      const acknowledged = this.readAcknowledgement(adminUserId);
      return {
        noticeVersion: this.currentNoticeVersion,
        acknowledgedAt: acknowledged.acknowledgedAt ?? iso(now),
      };
    })();
  }

  listGenerations() {
    const generatedAt = iso(this.clock());
    if (!isGenerationTablePresent(this.database)) return { generated_at: generatedAt, items: [] };
    const rows = this.database.prepare(`
      SELECT generation_id, owner_id, project_id, model_id, ratio, status,
        confirmed_credit_cost, reserved_credits, final_credit_state,
        error_category, created_at, updated_at
      FROM generation_jobs
      WHERE submission_ready = 1
      ORDER BY created_at DESC, generation_id DESC
      LIMIT 100
    `).all() as Array<{
      generation_id: string;
      owner_id: string;
      project_id: string;
      model_id: string;
      ratio: "3:4" | "1:1" | "4:3" | "9:16";
      status: GenerationStatus;
      confirmed_credit_cost: number;
      reserved_credits: number;
      final_credit_state: "committed" | "released" | null;
      error_category: string | null;
      created_at: number;
      updated_at: number;
    }>;
    return {
      generated_at: generatedAt,
      items: rows.map((row) => {
        const terminal = row.status === "succeeded" || row.status === "failed" || row.status === "rejected";
        return {
          generation_id: row.generation_id,
          owner_ref: row.owner_id,
          project_id: row.project_id,
          model_id: row.model_id,
          ratio: row.ratio,
          status: row.status,
          created_at: iso(row.created_at),
          completed_at: terminal ? iso(row.updated_at) : null,
          duration_ms: terminal ? Math.max(0, row.updated_at - row.created_at) : null,
          confirmed_credit_cost: row.confirmed_credit_cost,
          reserved_credits: row.reserved_credits,
          final_credit_state: row.final_credit_state,
          error_category: row.error_category,
        };
      }),
    };
  }

  private generation(generationId: string) {
    if (!isGenerationTablePresent(this.database)) throw new PrivateContentError("not_found");
    const row = this.database.prepare(`
      SELECT generation_id, owner_id, project_id
      FROM generation_jobs WHERE generation_id = ? AND submission_ready = 1
    `).get(generationId) as { generation_id: string; owner_id: string; project_id: string } | undefined;
    if (!row) throw new PrivateContentError("not_found");
    return row;
  }

  private recordAccess(input: { adminUserId: string; ownerId: string; generationId: string; contentType: "image" | "prompt" }) {
    const now = this.clock();
    // The insert is committed before the caller reads the private value. A failed
    // constraint therefore cannot accidentally release a private response.
    this.database.transaction(() => {
      this.database.prepare(`
        INSERT INTO private_content_access_logs (
          log_id, actor_ref, subject_ref, target_ref, content_type, occurred_at, expires_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run(
        randomUUID(), input.adminUserId, input.ownerId, input.generationId,
        input.contentType, now, now + auditRetentionMilliseconds,
      );
    })();
  }

  recordPrivateAssetAccess(adminUserId: string, ownerId: string, resourceId: string) {
    this.recordAccess({ adminUserId, ownerId, generationId: resourceId, contentType: "image" });
  }

  readPrompt(adminUserId: string, generationId: string) {
    this.requireAcknowledgement(adminUserId);
    const row = this.generation(generationId);
    this.recordAccess({ adminUserId, ownerId: row.owner_id, generationId: row.generation_id, contentType: "prompt" });
    const content = this.database.prepare(
      "SELECT prompt FROM generation_jobs WHERE generation_id = ? AND submission_ready = 1",
    ).get(row.generation_id) as { prompt: string } | undefined;
    if (!content) throw new PrivateContentError("not_found");
    return { generationId: row.generation_id, prompt: content.prompt };
  }

  readImageTarget(adminUserId: string, generationId: string) {
    this.requireAcknowledgement(adminUserId);
    const row = this.database.prepare(`
      SELECT g.generation_id, g.owner_id, g.project_id, pi.image_id
      FROM generation_jobs g
      JOIN project_images pi ON pi.project_id = g.project_id AND pi.generation_id = g.generation_id
      WHERE g.generation_id = ? AND g.status = 'succeeded'
      ORDER BY pi.created_at DESC LIMIT 1
    `).get(generationId) as { generation_id: string; owner_id: string; project_id: string; image_id: string } | undefined;
    if (!row) throw new PrivateContentError("not_found");
    this.recordAccess({ adminUserId, ownerId: row.owner_id, generationId: row.generation_id, contentType: "image" });
    return { projectId: row.project_id, imageId: row.image_id, ownerId: row.owner_id };
  }
}
