import { createHash, randomUUID } from "node:crypto";

import type {
  AssetReleaseManifestItem,
  AssetReleaseManifestProjection,
  AssetReleaseReader,
} from "@dada/asset-release-manifest";

import { auditRetentionMilliseconds, serializeAuditSummary } from "./audit-policy.js";
import type { RegistrationService } from "./registration.js";

export type PreviewBatchStatus = "active" | "closed";
export type PreviewGrantStatus = "active" | "revoked" | "expired";

export interface PreviewBatchView {
  batchId: string;
  createdAt: number;
  createdBy: string;
  name: string;
  status: PreviewBatchStatus;
}

export interface PreviewGrantView {
  batchId: string;
  expiresAt: number;
  grantId: string;
  grantedAt: number;
  grantedBy: string;
  status: PreviewGrantStatus;
  userId: string;
}

export class PreviewGrantError extends Error {
  constructor(
    public readonly reason:
      | "admin_invalid"
      | "batch_closed"
      | "batch_not_found"
      | "grant_not_found"
      | "invalid_expiry"
      | "invalid_request"
      | "resource_not_found"
      | "user_not_eligible",
  ) {
    super(reason);
    this.name = "PreviewGrantError";
  }
}

interface PreviewGrantServiceOptions {
  assetReleases: AssetReleaseReader;
  clock?: () => number;
  registration: RegistrationService;
}

interface PreviewManifestItemMapping {
  releaseVersion: string;
  resourceId: string;
  userId: string;
}

function isUuid(value: string) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
}

function assertText(value: string, name: string) {
  const normalized = value.trim();
  if (!normalized || normalized.length > 160) throw new PreviewGrantError("invalid_request");
  if (name === "batchId" && !isUuid(normalized)) throw new PreviewGrantError("invalid_request");
  return normalized;
}

function manifestHash(items: readonly AssetReleaseManifestItem[], releaseVersion: string) {
  return createHash("sha256")
    .update(JSON.stringify({
      items,
      release_version: releaseVersion,
      schema_version: "AssetReleaseManifest/v1",
    }))
    .digest("hex");
}

/**
 * Owns the P0-A preview grant state. Preview URLs are deliberately ephemeral:
 * the random item id is kept only in this process and every read rechecks the
 * persisted grant, so revocation and expiry take effect without cache busting.
 */
export class AssetPreviewGrantService {
  readonly database: RegistrationService["database"];
  readonly options: Required<Pick<PreviewGrantServiceOptions, "clock">> & PreviewGrantServiceOptions;
  private readonly itemMappings = new Map<string, PreviewManifestItemMapping>();

  constructor(options: PreviewGrantServiceOptions) {
    this.database = options.registration.database;
    this.options = { ...options, clock: options.clock ?? Date.now };
    this.migrate();
  }

  createBatch(input: { adminUserId: string; batchId?: string; name: string }): PreviewBatchView {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    const name = assertText(input.name, "name");
    const batchId = input.batchId ? assertText(input.batchId, "batchId") : randomUUID();
    const now = this.options.clock();
    this.assertAdmin(adminUserId, now);
    this.immediate(() => {
      this.database.prepare(`
        INSERT INTO test_batches (batch_id, name, status, created_by, created_at, closed_at)
        VALUES (?, ?, 'active', ?, ?, NULL)
      `).run(batchId, name, adminUserId, now);
      this.audit({
        actorRef: adminUserId,
        afterSummary: { batch_id: batchId, status: "active" },
        beforeSummary: null,
        operationType: "preview_batch_create",
        targetRef: batchId,
        targetType: "preview_batch",
      }, now);
    });
    return { batchId, createdAt: now, createdBy: adminUserId, name, status: "active" };
  }

  closeBatch(input: { adminUserId: string; batchId: string }): PreviewBatchView {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    const batchId = assertText(input.batchId, "batchId");
    const now = this.options.clock();
    this.assertAdmin(adminUserId, now);
    return this.immediate(() => {
      const batch = this.readBatch(batchId);
      if (!batch) throw new PreviewGrantError("batch_not_found");
      if (batch.status === "active") {
        this.database.prepare("UPDATE test_batches SET status = 'closed', closed_at = ? WHERE batch_id = ?").run(now, batchId);
        this.audit({
          actorRef: adminUserId,
          afterSummary: { batch_id: batchId, status: "closed" },
          beforeSummary: { batch_id: batchId, status: batch.status },
          operationType: "preview_batch_close",
          targetRef: batchId,
          targetType: "preview_batch",
        }, now);
      }
      return { ...batch, status: "closed" as const };
    });
  }

  addBatchItems(input: {
    adminUserId: string;
    batchId: string;
    releaseVersion: string;
    resourceIds: readonly string[];
  }) {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    const batchId = assertText(input.batchId, "batchId");
    const releaseVersion = assertText(input.releaseVersion, "releaseVersion");
    const resourceIds = [...new Set(input.resourceIds.map((resourceId) => assertText(resourceId, "resourceId")))];
    if (resourceIds.length === 0) throw new PreviewGrantError("invalid_request");
    const now = this.options.clock();
    this.assertAdmin(adminUserId, now);
    for (const resourceId of resourceIds) {
      if (!this.options.assetReleases.read("internal_preview_asset", releaseVersion, resourceId)) {
        throw new PreviewGrantError("resource_not_found");
      }
    }
    this.immediate(() => {
      const batch = this.readBatch(batchId);
      if (!batch) throw new PreviewGrantError("batch_not_found");
      if (batch.status !== "active") throw new PreviewGrantError("batch_closed");
      const insert = this.database.prepare(`
        INSERT OR IGNORE INTO test_batch_items (test_batch_id, release_version, resource_id)
        VALUES (?, ?, ?)
      `);
      for (const resourceId of resourceIds) insert.run(batchId, releaseVersion, resourceId);
      this.audit({
        actorRef: adminUserId,
        afterSummary: { batch_id: batchId, item_count: resourceIds.length, release_version: releaseVersion },
        beforeSummary: null,
        operationType: "preview_batch_items_add",
        targetRef: batchId,
        targetType: "preview_batch",
      }, now);
    });
    return { batchId, releaseVersion, resourceIds };
  }

  grant(input: {
    adminUserId: string;
    batchId: string;
    expiresAt: number;
    userId: string;
  }): PreviewGrantView {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    const batchId = assertText(input.batchId, "batchId");
    const userId = assertText(input.userId, "userId");
    if (!isUuid(userId)) throw new PreviewGrantError("invalid_request");
    const now = this.options.clock();
    if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= now) throw new PreviewGrantError("invalid_expiry");
    this.assertAdmin(adminUserId, now);
    return this.immediate(() => {
      const batch = this.readBatch(batchId);
      if (!batch) throw new PreviewGrantError("batch_not_found");
      if (batch.status !== "active") throw new PreviewGrantError("batch_closed");
      const user = this.database.prepare("SELECT role, status FROM users WHERE user_id = ?").get(userId) as { role: string; status: string } | undefined;
      if (!user || user.role !== "user" || user.status !== "active") throw new PreviewGrantError("user_not_eligible");
      const grantId = randomUUID();
      this.database.prepare(`
        INSERT INTO asset_preview_grants (
          grant_id, user_id, test_batch_id, granted_by, granted_at, expires_at, status
        ) VALUES (?, ?, ?, ?, ?, ?, 'active')
      `).run(grantId, userId, batchId, adminUserId, now, input.expiresAt);
      this.audit({
        actorRef: adminUserId,
        afterSummary: { batch_id: batchId, expires_at: input.expiresAt, grant_id: grantId, status: "active", user_id: userId },
        beforeSummary: null,
        operationType: "preview_grant_create",
        targetRef: grantId,
        targetType: "preview_grant",
      }, now);
      return {
        batchId,
        expiresAt: input.expiresAt,
        grantId,
        grantedAt: now,
        grantedBy: adminUserId,
        status: "active" as const,
        userId,
      };
    });
  }

  revoke(input: { adminUserId: string; grantId: string }): PreviewGrantView {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    const grantId = assertText(input.grantId, "grantId");
    const now = this.options.clock();
    this.assertAdmin(adminUserId, now);
    return this.immediate(() => {
      this.expireDue(now);
      const grant = this.readGrant(grantId);
      if (!grant) throw new PreviewGrantError("grant_not_found");
      if (grant.status === "active") {
        this.database.prepare("UPDATE asset_preview_grants SET status = 'revoked' WHERE grant_id = ? AND status = 'active'").run(grantId);
        this.audit({
          actorRef: adminUserId,
          afterSummary: { grant_id: grantId, status: "revoked" },
          beforeSummary: { grant_id: grantId, status: grant.status },
          operationType: "preview_grant_revoke",
          targetRef: grantId,
          targetType: "preview_grant",
        }, now);
      }
      return { ...grant, status: "revoked" as const };
    });
  }

  listBatches(input: { adminUserId: string }): PreviewBatchView[] {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    this.assertAdmin(adminUserId, this.options.clock());
    return (this.database.prepare(`
      SELECT batch_id, name, status, created_by, created_at
      FROM test_batches ORDER BY created_at DESC, batch_id DESC
    `).all() as Array<{ batch_id: string; created_at: number; created_by: string; name: string; status: PreviewBatchStatus }>).map((row) => ({
      batchId: row.batch_id,
      createdAt: row.created_at,
      createdBy: row.created_by,
      name: row.name,
      status: row.status,
    }));
  }

  listGrants(input: { adminUserId: string; batchId?: string; userId?: string }): PreviewGrantView[] {
    const adminUserId = assertText(input.adminUserId, "adminUserId");
    this.assertAdmin(adminUserId, this.options.clock());
    const batchId = input.batchId ? assertText(input.batchId, "batchId") : undefined;
    const userId = input.userId ? assertText(input.userId, "userId") : undefined;
    const now = this.options.clock();
    return this.immediate(() => {
      this.expireDue(now);
      const rows = this.database.prepare(`
        SELECT grant_id, user_id, test_batch_id, granted_by, granted_at, expires_at, status
        FROM asset_preview_grants
        WHERE (? IS NULL OR test_batch_id = ?) AND (? IS NULL OR user_id = ?)
        ORDER BY granted_at DESC, grant_id DESC
      `).all(batchId ?? null, batchId ?? null, userId ?? null, userId ?? null) as Array<{
        expires_at: number; grant_id: string; granted_at: number; granted_by: string;
        status: PreviewGrantStatus; test_batch_id: string; user_id: string;
      }>;
      return rows.map((row) => ({
        batchId: row.test_batch_id,
        expiresAt: row.expires_at,
        grantId: row.grant_id,
        grantedAt: row.granted_at,
        grantedBy: row.granted_by,
        status: row.status,
        userId: row.user_id,
      }));
    });
  }

  projectManifest(input: { releaseVersion: string; userId: string }): AssetReleaseManifestProjection | undefined {
    const releaseVersion = assertText(input.releaseVersion, "releaseVersion");
    const userId = assertText(input.userId, "userId");
    const base = this.options.assetReleases.project("internal_preview_asset", releaseVersion);
    if (!base) return undefined;
    const authorized = base.items.filter((item) => this.authorizeAsset({ releaseVersion, resourceId: item.resource_id, userId }));
    if (authorized.length === 0) return undefined;
    const items = authorized.map((item) => {
      const manifestItemId = randomUUID();
      const mapped: AssetReleaseManifestItem = {
        ...item,
        resource_id: manifestItemId,
        url: `/api/v1/assets/preview/${releaseVersion}/${manifestItemId}`,
      };
      this.itemMappings.set(manifestItemId, {
        releaseVersion,
        resourceId: item.resource_id,
        userId,
      });
      return mapped;
    });
    return Object.freeze({
      items: Object.freeze(items.map((item) => Object.freeze(item))),
      manifest_sha256: manifestHash(items, releaseVersion),
      release_version: releaseVersion,
      schema_version: "AssetReleaseManifest/v1" as const,
    });
  }

  authorizeAsset(input: { releaseVersion: string; resourceId: string; userId: string }) {
    const releaseVersion = assertText(input.releaseVersion, "releaseVersion");
    const resourceId = assertText(input.resourceId, "resourceId");
    const userId = assertText(input.userId, "userId");
    const now = this.options.clock();
    return this.immediate(() => {
      this.expireDue(now);
      const user = this.database.prepare("SELECT role, status FROM users WHERE user_id = ?").get(userId) as { role: string; status: string } | undefined;
      if (!user || user.role !== "user" || user.status !== "active") return false;
      const row = this.database.prepare(`
        SELECT 1 AS authorized
        FROM asset_preview_grants g
        JOIN test_batch_items i ON i.test_batch_id = g.test_batch_id
        WHERE g.user_id = ? AND g.status = 'active' AND g.expires_at > ?
          AND i.release_version = ? AND i.resource_id = ?
        LIMIT 1
      `).get(userId, now, releaseVersion, resourceId) as { authorized: 1 } | undefined;
      return Boolean(row);
    });
  }

  readManifestItem(input: { manifestItemId: string; releaseVersion: string; userId: string }) {
    const manifestItemId = assertText(input.manifestItemId, "manifestItemId");
    const releaseVersion = assertText(input.releaseVersion, "releaseVersion");
    const userId = assertText(input.userId, "userId");
    const mapping = this.itemMappings.get(manifestItemId);
    if (!mapping || mapping.releaseVersion !== releaseVersion || mapping.userId !== userId) return undefined;
    if (!this.authorizeAsset({ releaseVersion, resourceId: mapping.resourceId, userId })) {
      this.itemMappings.delete(manifestItemId);
      return undefined;
    }
    const resource = this.options.assetReleases.read("internal_preview_asset", releaseVersion, mapping.resourceId);
    return resource ? { ...resource, resourceId: manifestItemId } : undefined;
  }

  private migrate() {
    this.database.exec(`
      CREATE TABLE IF NOT EXISTS test_batches (
        batch_id TEXT PRIMARY KEY,
        name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 160),
        status TEXT NOT NULL CHECK (status IN ('active', 'closed')),
        created_by TEXT NOT NULL REFERENCES users(user_id),
        created_at INTEGER NOT NULL,
        closed_at INTEGER
      );
      CREATE TABLE IF NOT EXISTS test_batch_items (
        test_batch_id TEXT NOT NULL REFERENCES test_batches(batch_id),
        release_version TEXT NOT NULL,
        resource_id TEXT NOT NULL,
        PRIMARY KEY (test_batch_id, release_version, resource_id)
      );
      CREATE TABLE IF NOT EXISTS asset_preview_grants (
        grant_id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL REFERENCES users(user_id),
        test_batch_id TEXT NOT NULL REFERENCES test_batches(batch_id),
        granted_by TEXT NOT NULL REFERENCES users(user_id),
        granted_at INTEGER NOT NULL,
        expires_at INTEGER NOT NULL CHECK (expires_at > granted_at),
        status TEXT NOT NULL CHECK (status IN ('active', 'revoked', 'expired'))
      );
      CREATE INDEX IF NOT EXISTS asset_preview_grants_user_status
        ON asset_preview_grants(user_id, status, expires_at);
    `);
  }

  private immediate<T>(action: () => T): T {
    this.database.exec("BEGIN IMMEDIATE");
    try {
      const value = action();
      this.database.exec("COMMIT");
      return value;
    } catch (error) {
      if (this.database.inTransaction) this.database.exec("ROLLBACK");
      throw error;
    }
  }

  private assertAdmin(adminUserId: string, now: number) {
    const admin = this.database.prepare(`
      SELECT 1 AS allowed FROM users u JOIN admin_access a ON a.user_id = u.user_id
      WHERE u.user_id = ? AND u.role = 'super_admin' AND u.status = 'active' AND a.allowed = 1
    `).get(adminUserId) as { allowed: 1 } | undefined;
    if (!admin) throw new PreviewGrantError("admin_invalid");
    void now;
  }

  private readBatch(batchId: string): PreviewBatchView | undefined {
    const row = this.database.prepare(`
      SELECT batch_id, name, status, created_by, created_at
      FROM test_batches WHERE batch_id = ?
    `).get(batchId) as { batch_id: string; created_at: number; created_by: string; name: string; status: PreviewBatchStatus } | undefined;
    return row ? {
      batchId: row.batch_id,
      createdAt: row.created_at,
      createdBy: row.created_by,
      name: row.name,
      status: row.status,
    } : undefined;
  }

  private readGrant(grantId: string): PreviewGrantView | undefined {
    const row = this.database.prepare(`
      SELECT grant_id, user_id, test_batch_id, granted_by, granted_at, expires_at, status
      FROM asset_preview_grants WHERE grant_id = ?
    `).get(grantId) as {
      expires_at: number; grant_id: string; granted_at: number; granted_by: string;
      status: PreviewGrantStatus; test_batch_id: string; user_id: string;
    } | undefined;
    return row ? {
      batchId: row.test_batch_id,
      expiresAt: row.expires_at,
      grantId: row.grant_id,
      grantedAt: row.granted_at,
      grantedBy: row.granted_by,
      status: row.status,
      userId: row.user_id,
    } : undefined;
  }

  private expireDue(now: number) {
    const rows = this.database.prepare(`
      SELECT grant_id, user_id, test_batch_id FROM asset_preview_grants
      WHERE status = 'active' AND expires_at <= ?
    `).all(now) as Array<{ grant_id: string; test_batch_id: string; user_id: string }>;
    if (rows.length === 0) return;
    this.database.prepare("UPDATE asset_preview_grants SET status = 'expired' WHERE status = 'active' AND expires_at <= ?").run(now);
    for (const row of rows) {
      this.audit({
        actorRef: "preview_grant_expiry",
        afterSummary: { grant_id: row.grant_id, status: "expired" },
        beforeSummary: { grant_id: row.grant_id, status: "active" },
        operationType: "preview_grant_expire",
        targetRef: row.grant_id,
        targetType: "preview_grant",
      }, now, "system");
    }
  }

  private audit(input: {
    actorRef: string;
    afterSummary: Record<string, unknown> | null;
    beforeSummary: Record<string, unknown> | null;
    operationType: string;
    targetRef: string;
    targetType: string;
  }, now: number, actorType: "super_admin" | "system" = "super_admin") {
    this.database.prepare(`
      INSERT INTO admin_operation_logs (
        log_id, actor_type, actor_ref, operation_type, target_type, target_ref,
        result, before_summary, after_summary, occurred_at, expires_at
      ) VALUES (?, ?, ?, ?, ?, ?, 'succeeded', ?, ?, ?, ?)
    `).run(
      randomUUID(), actorType, input.actorRef, input.operationType, input.targetType, input.targetRef,
      serializeAuditSummary(input.beforeSummary), serializeAuditSummary(input.afterSummary),
      now, now + auditRetentionMilliseconds,
    );
  }
}
