import { randomUUID } from 'node:crypto';
import { HttpError, type RequestContext } from '../../core/http.js';
import { intParam, readJsonBody, requiredString, stringParam } from '../../core/request.js';
import { query, queryOne } from '../../core/db.js';
import { requireTenantContentEditor, type TenantContentAuth } from './auth.js';
import { boolValue, intValue, jsonObjectValue, nullableString } from './utils.js';
import {
  assertUploadProvider,
  configuredDefaultStorageBucket,
  assertWritableLocation,
  configuredDefaultStorageProvider,
  headStorageObject,
  normalizeStorageProvider,
  signStorageDownload,
  signStorageUpload,
  validateFileSize,
  validateMimeType,
  validateObjectKey,
  type StorageProviderName,
} from '../storage/service.js';
import {
  assetAccessTtl,
  assertCdnAccessAllowed,
  buildAssetWatermarkContext,
  recordAssetAccessEvent,
  signedAssetFingerprint,
} from '../storage/asset-access.js';

const ASSET_TYPES = ['pdf', 'video', 'image', 'audio', 'document', 'package', 'link', 'other'];
const STORAGE_PROVIDERS = ['external_url', 'supabase_storage', 'aliyun_oss', 'tencent_cos', 'qiniu_kodo', 'local_dev'];
const VISIBILITIES = ['public', 'tenant', 'members', 'svip', 'private'];
const ASSET_STATUSES = ['draft', 'active', 'archived'];
const UPLOAD_STATUSES = ['not_required', 'pending', 'verified', 'failed'];
const PREVIEW_STATUSES = ['none', 'pending', 'ready', 'failed'];
const SECURITY_SCAN_STATUSES = ['not_required', 'pending', 'scanning', 'passed', 'failed', 'skipped'];

interface AssetRow {
  id: string;
  tenantId: string;
  assetType: string;
  storageProvider: string;
  bucket: string | null;
  objectKey: string | null;
  previewObjectKey: string | null;
  title: string | null;
  fileName: string | null;
  cdnUrl: string | null;
  previewUrl: string | null;
  mimeType: string | null;
  fileSizeBytes: number | null;
  checksumSha256: string | null;
  visibility: string;
  status: string;
  uploadStatus: string;
  securityScanStatus: string;
  securityScanProvider: string | null;
  securityScanSummary: Record<string, unknown>;
  verifiedSizeBytes: number | null;
  verifiedChecksumSha256: string | null;
  previewStatus: string;
  accessRules?: Record<string, unknown>;
  metadata?: Record<string, unknown>;
}

function choice(value: unknown, allowed: string[], fallback: string, label: string) {
  const candidate = nullableString(value) || fallback;
  if (!allowed.includes(candidate)) {
    throw new HttpError(400, `Invalid ${label}: ${candidate}`, 'INVALID_FIELD_VALUE');
  }
  return candidate;
}

function nullableUuid(value: unknown) {
  return nullableString(value);
}

function assertUuidParam(value: string, label: string) {
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
    throw new HttpError(400, `${label} must be a valid UUID`, 'INVALID_UUID');
  }
}

function safeFileName(fileName: string) {
  return fileName
    .trim()
    .replace(/[\\/:*?"<>|]+/g, '-')
    .replace(/\s+/g, '-')
    .slice(0, 160) || 'asset';
}

function isManagedObjectProvider(provider: StorageProviderName) {
  return provider === 'local_dev' || provider === 'supabase_storage' || provider === 'aliyun_oss' || provider === 'tencent_cos';
}

function checksumSha256(value: unknown) {
  const raw = nullableString(value);
  const checksum = raw ? raw.toLowerCase() : null;
  if (!checksum) return null;
  if (!/^[a-f0-9]{64}$/.test(checksum)) {
    throw new HttpError(400, 'checksumSha256 must be a lowercase hex SHA-256 digest', 'INVALID_CHECKSUM');
  }
  return checksum;
}

function canonicalMime(value: string | null) {
  return value?.split(';')[0]?.trim().toLowerCase() || null;
}

function nullableNumber(value: unknown) {
  if (value === null || value === undefined || value === '') return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
}

function assetPreviewable(asset: Pick<AssetRow, 'assetType' | 'mimeType'>) {
  const mime = canonicalMime(asset.mimeType);
  return asset.assetType === 'pdf' || mime === 'application/pdf' || asset.assetType === 'image' || Boolean(mime?.startsWith('image/'));
}

async function existingAssetForUpsert(tenantId: string, id: string | null) {
  if (!id) return null;
  return queryOne<AssetRow>(
    `
    select id, tenant_id as "tenantId", asset_type as "assetType",
           storage_provider as "storageProvider", bucket, object_key as "objectKey",
           preview_object_key as "previewObjectKey", title, file_name as "fileName",
           cdn_url as "cdnUrl", preview_url as "previewUrl", mime_type as "mimeType",
           file_size_bytes as "fileSizeBytes", checksum_sha256 as "checksumSha256",
           visibility, status, upload_status as "uploadStatus",
           security_scan_status as "securityScanStatus",
           security_scan_provider as "securityScanProvider",
           security_scan_summary as "securityScanSummary",
           verified_size_bytes as "verifiedSizeBytes",
           verified_checksum_sha256 as "verifiedChecksumSha256",
           preview_status as "previewStatus"
    from public.content_assets
    where tenant_id = $1 and id = $2
    limit 1
    `,
    [tenantId, id],
  );
}

function sameObjectLocation(existing: AssetRow | null, provider: StorageProviderName, bucket: string | null, objectKey: string | null) {
  return Boolean(
    existing &&
      existing.storageProvider === provider &&
      (existing.bucket || null) === (bucket || null) &&
      (existing.objectKey || null) === (objectKey || null),
  );
}

function objectUploadStatus(input: {
  existing: AssetRow | null;
  provider: StorageProviderName;
  bucket: string | null;
  objectKey: string | null;
  cdnUrl: string | null;
}) {
  if (!input.objectKey || !isManagedObjectProvider(input.provider)) return 'not_required';
  if (sameObjectLocation(input.existing, input.provider, input.bucket, input.objectKey)) {
    return input.existing?.uploadStatus || 'pending';
  }
  return 'pending';
}

function objectSecurityScanStatus(input: {
  existing: AssetRow | null;
  provider: StorageProviderName;
  objectKey: string | null;
  uploadStatus: string;
}) {
  if (!input.objectKey || !isManagedObjectProvider(input.provider)) return 'not_required';
  if (input.uploadStatus !== 'verified') return 'pending';
  return input.existing?.securityScanStatus || 'pending';
}

export function assertAssetSecurityScanPassed(input: {
  provider: StorageProviderName;
  objectKey: string | null;
  securityScanStatus?: string | null;
}) {
  if (!input.objectKey || !isManagedObjectProvider(input.provider)) return;
  if (input.securityScanStatus === 'passed') return;
  if (input.securityScanStatus === 'failed') {
    throw new HttpError(409, 'Asset security scan failed', 'ASSET_SECURITY_SCAN_FAILED');
  }
  throw new HttpError(409, 'Asset security scan is required before access', 'ASSET_SECURITY_SCAN_REQUIRED');
}

function assertPublishableManagedObject(
  status: string,
  uploadStatus: string,
  scanStatus: string,
  provider: StorageProviderName,
  objectKey: string | null,
) {
  if (status !== 'active' || !objectKey || !isManagedObjectProvider(provider)) return;
  if (uploadStatus !== 'verified') {
    throw new HttpError(
      409,
      'Managed storage asset must be confirmed before publishing',
      'ASSET_UPLOAD_CONFIRMATION_REQUIRED',
    );
  }
  assertAssetSecurityScanPassed({ provider, objectKey, securityScanStatus: scanStatus });
}

async function assertOptionalReference(tenantId: string, table: string, id: string | null, code: string) {
  if (!id) return;
  const row = await queryOne<{ id: string }>(
    `select id from public.${table} where tenant_id = $1 and id = $2 limit 1`,
    [tenantId, id],
  );
  if (!row) {
    throw new HttpError(400, `${table} reference is not in this tenant`, code);
  }
}

async function recordAssetAudit(auth: TenantContentAuth, action: string, targetId: string | null, details: Record<string, unknown>) {
  await query(
    `
    insert into public.audit_logs (tenant_id, actor_user_id, action, target_type, target_id, details)
    values ($1, $2, $3, 'content_asset', $4, $5::jsonb)
    `,
    [auth.tenantId, auth.userId, action, targetId, JSON.stringify(details)],
  );
}

async function recordDeniedAdminAssetAccess(ctx: RequestContext, auth: TenantContentAuth, asset: AssetRow, accessType: 'admin_download' | 'admin_preview', error: unknown) {
  const denyCode = error instanceof HttpError ? error.code : 'ASSET_ACCESS_DENIED';
  await recordAssetAccessEvent({
    ctx,
    tenantId: auth.tenantId,
    assetId: asset.id,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType,
    visibility: asset.visibility,
    assetType: asset.assetType,
    storageProvider: asset.storageProvider,
    result: 'denied',
    denyCode,
    metadata: {
      title: asset.title,
      fileName: asset.fileName,
    },
  });
}

export async function assetsAdminRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const limit = intParam(ctx, 'limit', 100, 500);
  const assetType = stringParam(ctx, 'assetType');
  const status = stringParam(ctx, 'status');
  const visibility = stringParam(ctx, 'visibility');
  const regionId = stringParam(ctx, 'regionId');
  const subjectId = stringParam(ctx, 'subjectId');
  const categoryId = stringParam(ctx, 'categoryId');
  const entryId = stringParam(ctx, 'entryId');
  const contentNodeId = stringParam(ctx, 'contentNodeId');
  const securityScanStatus = stringParam(ctx, 'securityScanStatus');

  const params: unknown[] = [auth.tenantId];
  const filters = ['tenant_id = $1'];
  if (assetType) {
    params.push(assetType);
    filters.push(`asset_type = $${params.length}`);
  }
  if (status) {
    params.push(status);
    filters.push(`status = $${params.length}`);
  }
  if (visibility) {
    params.push(visibility);
    filters.push(`visibility = $${params.length}`);
  }
  if (regionId) {
    params.push(regionId);
    filters.push(`region_id = $${params.length}`);
  }
  if (subjectId) {
    params.push(subjectId);
    filters.push(`subject_id = $${params.length}`);
  }
  if (categoryId) {
    params.push(categoryId);
    filters.push(`category_id = $${params.length}`);
  }
  if (entryId) {
    params.push(entryId);
    filters.push(`entry_id = $${params.length}`);
  }
  if (contentNodeId) {
    params.push(contentNodeId);
    filters.push(`content_node_id = $${params.length}`);
  }
  if (securityScanStatus) {
    if (!SECURITY_SCAN_STATUSES.includes(securityScanStatus)) {
      throw new HttpError(400, `Invalid securityScanStatus: ${securityScanStatus}`, 'INVALID_FIELD_VALUE');
    }
    params.push(securityScanStatus);
    filters.push(`security_scan_status = $${params.length}`);
  }
  params.push(limit);

  const items = await query(
    `
    select id, legacy_id as "legacyId", asset_key as "assetKey",
           asset_type as "assetType", storage_provider as "storageProvider",
           bucket, object_key as "objectKey", title, category as "categoryLabel",
           description, file_name as "fileName", cdn_url as "cdnUrl",
           preview_url as "previewUrl", mime_type as "mimeType",
           file_size_bytes as "fileSizeBytes", checksum_sha256 as "checksumSha256",
           upload_status as "uploadStatus", verified_at as "verifiedAt",
           verified_by as "verifiedBy", verified_size_bytes as "verifiedSizeBytes",
           verified_checksum_sha256 as "verifiedChecksumSha256",
           verification_details as "verificationDetails",
           preview_object_key as "previewObjectKey", preview_status as "previewStatus",
           security_scan_status as "securityScanStatus",
           security_scanned_at as "securityScannedAt",
           security_scan_provider as "securityScanProvider",
           security_scan_summary as "securityScanSummary",
           security_flags as "securityFlags",
           visibility, is_public as "isPublic", region_id as "regionId",
           subject_id as "subjectId", category_id as "categoryId", node_id as "nodeId",
           entry_id as "entryId", content_node_id as "contentNodeId",
           status, sort_order as "order", access_rules as "accessRules",
           source, download_count as "downloadCount", metadata,
           created_by as "createdBy", updated_by as "updatedBy",
           created_at as "createdAt", updated_at as "updatedAt"
    from public.content_assets
    where ${filters.join(' and ')}
    order by sort_order asc, created_at desc
    limit $${params.length}
    `,
    params,
  );

  return { items };
}

export async function assetSecurityScanEventsAdminRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const limit = intParam(ctx, 'limit', 100, 500);
  const assetId = stringParam(ctx, 'assetId');
  const scanStatus = stringParam(ctx, 'scanStatus');

  const params: unknown[] = [auth.tenantId];
  const filters = ['tenant_id = $1'];
  if (assetId) {
    assertUuidParam(assetId, 'assetId');
    params.push(assetId);
    filters.push(`asset_id = $${params.length}::uuid`);
  }
  if (scanStatus) {
    if (!SECURITY_SCAN_STATUSES.includes(scanStatus) || scanStatus === 'not_required') {
      throw new HttpError(400, `Invalid scanStatus: ${scanStatus}`, 'INVALID_FIELD_VALUE');
    }
    params.push(scanStatus);
    filters.push(`scan_status = $${params.length}`);
  }
  params.push(limit);

  const items = await query(
    `
    select id, asset_id as "assetId", provider,
           scan_status as "scanStatus", risk_level as "riskLevel",
           issue_codes as "issueCodes", details, created_at as "createdAt"
    from public.content_asset_security_scan_events
    where ${filters.join(' and ')}
    order by created_at desc
    limit $${params.length}
    `,
    params,
  );

  return { items };
}

export async function assetAccessEventsAdminRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const limit = intParam(ctx, 'limit', 100, 500);
  const assetId = stringParam(ctx, 'assetId');

  const params: unknown[] = [auth.tenantId];
  const filters = ['tenant_id = $1'];
  if (assetId) {
    assertUuidParam(assetId, 'assetId');
    params.push(assetId);
    filters.push(`asset_id = $${params.length}::uuid`);
  }
  params.push(limit);

  const items = await query(
    `
    select id, asset_id as "assetId", user_id as "userId",
           actor_role as "actorRole", access_type as "accessType",
           visibility, asset_type as "assetType",
           storage_provider as "storageProvider", disposition,
           expires_in_sec as "expiresInSec", signature_mode as "signatureMode",
           result, deny_code as "denyCode", ip_address as "ipAddress",
           user_agent as "userAgent", metadata, created_at as "createdAt"
    from public.content_asset_access_events
    where ${filters.join(' and ')}
    order by created_at desc
    limit $${params.length}
    `,
    params,
  );

  return { items };
}

export async function upsertAssetRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const body = await readJsonBody(ctx);

  const assetType = choice(body.assetType, ASSET_TYPES, 'document', 'assetType');
  const storageProvider = choice(
    body.storageProvider,
    STORAGE_PROVIDERS,
    nullableString(body.cdnUrl) ? 'external_url' : configuredDefaultStorageProvider(),
    'storageProvider',
  ) as StorageProviderName;
  const visibility = choice(body.visibility, VISIBILITIES, boolValue(body.isPublic, false) ? 'public' : 'tenant', 'visibility');
  const bucket = nullableString(body.bucket);
  const objectKey = nullableString(body.objectKey);
  const cdnUrl = nullableString(body.cdnUrl);
  const title = requiredString(body, 'title');
  const existing = await existingAssetForUpsert(auth.tenantId, nullableString(body.id));
  const regionId = nullableUuid(body.regionId);
  const subjectId = nullableUuid(body.subjectId);
  const categoryId = nullableUuid(body.categoryId);
  const nodeId = nullableUuid(body.nodeId);
  const entryId = nullableUuid(body.entryId);
  const contentNodeId = nullableUuid(body.contentNodeId);

  const cleanObjectKey = objectKey ? validateObjectKey(auth.tenantId, objectKey) : null;
  const previewObjectKey = nullableString(body.previewObjectKey)
    ? validateObjectKey(auth.tenantId, requiredString(body, 'previewObjectKey'))
    : null;
  const mimeType = nullableString(body.mimeType);
  if (mimeType) validateMimeType(mimeType);
  const fileSizeBytes = body.fileSizeBytes === undefined ? null : validateFileSize(intValue(body.fileSizeBytes, 0));
  const checksum = checksumSha256(body.checksumSha256);
  assertWritableLocation({ provider: storageProvider, bucket, objectKey: cleanObjectKey, cdnUrl });
  const nextUploadStatus = objectUploadStatus({
    existing,
    provider: storageProvider,
    bucket,
    objectKey: cleanObjectKey,
    cdnUrl,
  });
  const nextSecurityScanStatus = objectSecurityScanStatus({
    existing,
    provider: storageProvider,
    objectKey: cleanObjectKey,
    uploadStatus: nextUploadStatus,
  });
  const nextPreviewStatus = previewObjectKey || nullableString(body.previewUrl)
    ? choice(body.previewStatus, PREVIEW_STATUSES, existing?.previewStatus || 'ready', 'previewStatus')
    : 'none';
  const statusFallback = cleanObjectKey
    && isManagedObjectProvider(storageProvider)
    && (nextUploadStatus !== 'verified' || nextSecurityScanStatus !== 'passed')
    ? 'draft'
    : 'active';
  const status = choice(body.status, ASSET_STATUSES, statusFallback, 'status');
  assertPublishableManagedObject(status, nextUploadStatus, nextSecurityScanStatus, storageProvider, cleanObjectKey);

  if (status === 'active' && !cdnUrl && !objectKey) {
    throw new HttpError(400, 'Active asset requires cdnUrl or objectKey', 'ASSET_LOCATION_REQUIRED');
  }

  await assertOptionalReference(auth.tenantId, 'regions', regionId, 'REGION_NOT_FOUND');
  await assertOptionalReference(auth.tenantId, 'subjects', subjectId, 'SUBJECT_NOT_FOUND');
  await assertOptionalReference(auth.tenantId, 'categories', categoryId, 'CATEGORY_NOT_FOUND');
  await assertOptionalReference(auth.tenantId, 'module_nodes', nodeId, 'NODE_NOT_FOUND');
  await assertOptionalReference(auth.tenantId, 'content_entries', entryId, 'ENTRY_NOT_FOUND');
  await assertOptionalReference(auth.tenantId, 'content_nodes', contentNodeId, 'CONTENT_NODE_NOT_FOUND');

  const item = await queryOne(
    `
    insert into public.content_assets (
      id, tenant_id, legacy_id, asset_key, asset_type, storage_provider,
      bucket, object_key, title, category, description, file_name, cdn_url,
      preview_url, mime_type, file_size_bytes, checksum_sha256,
      upload_status, security_scan_status, security_scanned_at,
      security_scan_provider, security_scan_summary,
      preview_object_key, preview_status, visibility,
      is_public, region_id, subject_id, category_id, node_id, entry_id,
      content_node_id, status,
      sort_order, access_rules, metadata, created_by, updated_by, source
    )
    values (
      coalesce($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6,
      $7, $8, $9, $10, $11, $12, $13,
      $14, $15, $16, $17, $18,
      $19, case when $19 in ('passed', 'not_required') then now() else null end,
      $20, $21::jsonb,
      $22, $23, $24,
      $25, $26::uuid, $27::uuid, $28::uuid, $29::uuid, $30::uuid,
      $31::uuid, $32,
      $33, $34::jsonb, $35::jsonb, $36, $36, $37
    )
    on conflict (id)
    do update set legacy_id = excluded.legacy_id,
                  asset_key = excluded.asset_key,
                  asset_type = excluded.asset_type,
                  storage_provider = excluded.storage_provider,
                  bucket = excluded.bucket,
                  object_key = excluded.object_key,
                  title = excluded.title,
                  category = excluded.category,
                  description = excluded.description,
                  file_name = excluded.file_name,
                  cdn_url = excluded.cdn_url,
                  preview_url = excluded.preview_url,
                  mime_type = excluded.mime_type,
                  file_size_bytes = excluded.file_size_bytes,
                  checksum_sha256 = excluded.checksum_sha256,
                  upload_status = excluded.upload_status,
                  security_scan_status = excluded.security_scan_status,
                  security_scanned_at = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                     and excluded.security_scan_status = public.content_assets.security_scan_status
                    then public.content_assets.security_scanned_at
                    else excluded.security_scanned_at
                  end,
                  security_scan_provider = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                     and excluded.security_scan_status = public.content_assets.security_scan_status
                    then public.content_assets.security_scan_provider
                    else excluded.security_scan_provider
                  end,
                  security_scan_summary = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                     and excluded.security_scan_status = public.content_assets.security_scan_status
                    then public.content_assets.security_scan_summary
                    else excluded.security_scan_summary
                  end,
                  verified_at = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                    then public.content_assets.verified_at
                    else null
                  end,
                  verified_by = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                    then public.content_assets.verified_by
                    else null
                  end,
                  verified_size_bytes = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                    then public.content_assets.verified_size_bytes
                    else null
                  end,
                  verified_checksum_sha256 = case
                    when public.content_assets.storage_provider = excluded.storage_provider
                     and coalesce(public.content_assets.bucket, '') = coalesce(excluded.bucket, '')
                     and coalesce(public.content_assets.object_key, '') = coalesce(excluded.object_key, '')
                    then public.content_assets.verified_checksum_sha256
                    else null
                  end,
                  preview_object_key = excluded.preview_object_key,
                  preview_status = excluded.preview_status,
                  visibility = excluded.visibility,
                  is_public = excluded.is_public,
                  region_id = excluded.region_id,
                  subject_id = excluded.subject_id,
                  category_id = excluded.category_id,
                  node_id = excluded.node_id,
                  entry_id = excluded.entry_id,
                  content_node_id = excluded.content_node_id,
                  status = excluded.status,
                  sort_order = excluded.sort_order,
                  access_rules = excluded.access_rules,
                  metadata = excluded.metadata,
                  updated_by = excluded.updated_by,
                  source = excluded.source,
                  updated_at = now()
    where public.content_assets.tenant_id = excluded.tenant_id
    returning id, legacy_id as "legacyId", asset_key as "assetKey",
              asset_type as "assetType", storage_provider as "storageProvider",
              bucket, object_key as "objectKey", title, category as "categoryLabel",
              description, file_name as "fileName", cdn_url as "cdnUrl",
              preview_url as "previewUrl", mime_type as "mimeType",
              file_size_bytes as "fileSizeBytes", checksum_sha256 as "checksumSha256",
              upload_status as "uploadStatus", verified_at as "verifiedAt",
              verified_by as "verifiedBy", verified_size_bytes as "verifiedSizeBytes",
              verified_checksum_sha256 as "verifiedChecksumSha256",
              verification_details as "verificationDetails",
              preview_object_key as "previewObjectKey", preview_status as "previewStatus",
              security_scan_status as "securityScanStatus",
              security_scanned_at as "securityScannedAt",
              security_scan_provider as "securityScanProvider",
              security_scan_summary as "securityScanSummary",
              security_flags as "securityFlags",
              visibility, is_public as "isPublic", region_id as "regionId",
              subject_id as "subjectId", category_id as "categoryId", node_id as "nodeId",
              entry_id as "entryId", content_node_id as "contentNodeId",
              status, sort_order as "order", access_rules as "accessRules",
              source, download_count as "downloadCount", metadata,
              created_by as "createdBy", updated_by as "updatedBy",
              created_at as "createdAt", updated_at as "updatedAt"
    `,
    [
      nullableString(body.id),
      auth.tenantId,
      nullableString(body.legacyId),
      nullableString(body.assetKey),
      assetType,
      storageProvider,
      bucket,
      cleanObjectKey,
      title,
      nullableString(body.categoryLabel) || nullableString(body.category),
      nullableString(body.description),
      nullableString(body.fileName),
      cdnUrl,
      nullableString(body.previewUrl),
      mimeType,
      fileSizeBytes,
      checksum,
      nextUploadStatus,
      nextSecurityScanStatus,
      nextSecurityScanStatus === 'not_required' ? null : existing?.securityScanProvider || 'metadata_rules',
      JSON.stringify(
        nextSecurityScanStatus === 'not_required'
          ? {}
          : nextSecurityScanStatus === 'passed'
            ? existing?.securityScanSummary || { riskLevel: 'none', issueCodes: [] }
            : { riskLevel: 'unknown', issueCodes: [], pendingReason: 'asset_upsert' },
      ),
      previewObjectKey,
      nextPreviewStatus,
      visibility,
      visibility === 'public',
      regionId,
      subjectId,
      categoryId,
      nodeId,
      entryId,
      contentNodeId,
      status,
      intValue(body.order, 0),
      jsonObjectValue(body.accessRules),
      jsonObjectValue(body.metadata),
      auth.userId,
      nullableString(body.source) || 'manual',
    ],
  );

  if (!item) {
    throw new HttpError(404, 'Asset not found in this tenant', 'ASSET_NOT_FOUND');
  }

  await recordAssetAudit(auth, 'content.asset.upserted', String((item as { id?: string } | null)?.id || ''), {
    title,
    assetType,
    visibility,
    status,
  });

  return { item };
}

export async function confirmAssetUploadRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const body = await readJsonBody(ctx);
  const assetId = requiredString(body, 'assetId');

  const asset = await queryOne<AssetRow>(
    `
    select id, tenant_id as "tenantId", asset_type as "assetType",
           storage_provider as "storageProvider", bucket, object_key as "objectKey",
           preview_object_key as "previewObjectKey", title, file_name as "fileName",
           cdn_url as "cdnUrl", preview_url as "previewUrl", mime_type as "mimeType",
           file_size_bytes as "fileSizeBytes", checksum_sha256 as "checksumSha256",
           visibility, status, upload_status as "uploadStatus",
           security_scan_status as "securityScanStatus",
           security_scan_provider as "securityScanProvider",
           security_scan_summary as "securityScanSummary",
           verified_size_bytes as "verifiedSizeBytes",
           verified_checksum_sha256 as "verifiedChecksumSha256",
           preview_status as "previewStatus"
    from public.content_assets
    where tenant_id = $1 and id = $2
    limit 1
    `,
    [auth.tenantId, assetId],
  );

  if (!asset) throw new HttpError(404, 'Asset not found', 'ASSET_NOT_FOUND');
  const provider = asset.storageProvider as StorageProviderName;
  if (!asset.objectKey || !isManagedObjectProvider(provider)) {
    throw new HttpError(400, 'Asset does not require upload confirmation', 'UPLOAD_CONFIRM_NOT_REQUIRED');
  }

  const declaredFileSize = body.fileSizeBytes === undefined
    ? asset.fileSizeBytes
    : validateFileSize(intValue(body.fileSizeBytes, 0));
  const registeredFileSize = nullableNumber(asset.fileSizeBytes);
  const normalizedDeclaredFileSize = nullableNumber(declaredFileSize);
  const declaredMimeType = nullableString(body.mimeType) || asset.mimeType;
  const declaredChecksum = checksumSha256(body.checksumSha256) || asset.checksumSha256;

  const metadata = await headStorageObject({
    tenantId: auth.tenantId,
    provider,
    bucket: asset.bucket,
    objectKey: asset.objectKey,
    declaredFileSizeBytes: normalizedDeclaredFileSize,
    declaredMimeType,
    declaredChecksumSha256: declaredChecksum,
  });

  const issues: string[] = [];
  if (registeredFileSize !== null && body.fileSizeBytes !== undefined && normalizedDeclaredFileSize !== registeredFileSize) {
    issues.push('declared_file_size_changed');
  }
  if (asset.mimeType && body.mimeType !== undefined && canonicalMime(declaredMimeType) !== canonicalMime(asset.mimeType)) {
    issues.push('declared_mime_type_changed');
  }
  if (asset.checksumSha256 && body.checksumSha256 !== undefined && declaredChecksum !== asset.checksumSha256) {
    issues.push('declared_checksum_changed');
  }
  if (normalizedDeclaredFileSize !== null && metadata.sizeBytes !== null && normalizedDeclaredFileSize !== metadata.sizeBytes) {
    issues.push('file_size_mismatch');
  }
  const expectedMime = declaredMimeType ? canonicalMime(validateMimeType(declaredMimeType)) : null;
  const observedMime = canonicalMime(metadata.mimeType);
  if (expectedMime && observedMime && expectedMime !== observedMime) {
    issues.push('mime_type_mismatch');
  }
  if (declaredChecksum && metadata.checksumSha256 && declaredChecksum !== metadata.checksumSha256) {
    issues.push('checksum_mismatch');
  }

  const verificationDetails = {
    metadata,
    declared: {
      fileSizeBytes: normalizedDeclaredFileSize,
      mimeType: expectedMime,
      checksumSha256: declaredChecksum,
    },
    issues,
    checksumVerified: Boolean(declaredChecksum && metadata.checksumSha256 && declaredChecksum === metadata.checksumSha256),
    checksumUnavailable: Boolean(declaredChecksum && !metadata.checksumSha256),
  };

  if (issues.length) {
    await query(
      `
      update public.content_assets
      set upload_status = 'failed',
          security_scan_status = 'skipped',
          security_scanned_at = null,
          security_scan_provider = 'metadata_rules',
          security_scan_summary = $5::jsonb,
          verification_details = $3::jsonb,
          security_flags = jsonb_set(coalesce(security_flags, '{}'::jsonb), '{uploadVerificationFailed}', 'true'::jsonb, true),
          updated_by = $4,
          updated_at = now()
      where tenant_id = $1 and id = $2
      `,
      [
        auth.tenantId,
        assetId,
        JSON.stringify(verificationDetails),
        auth.userId,
        JSON.stringify({ riskLevel: 'medium', issueCodes: issues, skippedReason: 'upload_verification_failed' }),
      ],
    );
    await recordAssetAccessEvent({
      ctx,
      tenantId: auth.tenantId,
      assetId,
      userId: auth.userId,
      actorRole: 'tenant_content_editor',
      accessType: 'upload_confirm',
      visibility: asset.visibility,
      assetType: asset.assetType,
      storageProvider: asset.storageProvider,
      result: 'denied',
      denyCode: 'UPLOAD_VERIFICATION_FAILED',
      metadata: {
        provider,
        bucket: asset.bucket,
        objectKey: asset.objectKey,
        issues,
      },
    });
    throw new HttpError(409, `Upload verification failed: ${issues.join(', ')}`, 'UPLOAD_VERIFICATION_FAILED');
  }

  const publish = body.publish === true;
  const item = await queryOne(
    `
    update public.content_assets
    set upload_status = 'verified',
        verified_at = now(),
        verified_by = $3,
        verified_size_bytes = coalesce($4::bigint, verified_size_bytes),
        verified_checksum_sha256 = coalesce($5, verified_checksum_sha256),
        file_size_bytes = coalesce(file_size_bytes, $4::bigint),
        mime_type = coalesce(mime_type, $6),
        checksum_sha256 = coalesce(checksum_sha256, $5),
        verification_details = $7::jsonb,
        security_scan_status = 'pending',
        security_scanned_at = null,
        security_scan_provider = 'metadata_rules',
        security_scan_summary = '{"riskLevel":"unknown","issueCodes":[],"pendingReason":"upload_confirmed"}'::jsonb,
        security_flags = coalesce(security_flags, '{}'::jsonb) - 'assetSecurityScanFailed',
        status = case when status = 'archived' then status else 'draft' end,
        updated_by = $3,
        updated_at = now()
    where tenant_id = $1 and id = $2
    returning id, asset_type as "assetType", storage_provider as "storageProvider",
              bucket, object_key as "objectKey", title, file_name as "fileName",
              mime_type as "mimeType", file_size_bytes as "fileSizeBytes",
              checksum_sha256 as "checksumSha256", visibility, status,
              upload_status as "uploadStatus", verified_at as "verifiedAt",
              verified_by as "verifiedBy", verified_size_bytes as "verifiedSizeBytes",
              verified_checksum_sha256 as "verifiedChecksumSha256",
              verification_details as "verificationDetails",
              security_scan_status as "securityScanStatus",
              security_scanned_at as "securityScannedAt",
              security_scan_provider as "securityScanProvider",
              security_scan_summary as "securityScanSummary"
    `,
    [
      auth.tenantId,
      assetId,
      auth.userId,
      metadata.sizeBytes ?? normalizedDeclaredFileSize,
      metadata.checksumSha256 ?? declaredChecksum,
      observedMime || expectedMime,
      JSON.stringify(verificationDetails),
    ],
  );

  await recordAssetAudit(auth, 'content.asset.upload_confirmed', assetId, {
    provider,
    bucket: asset.bucket,
    objectKey: asset.objectKey,
    publish,
    checksumVerified: verificationDetails.checksumVerified,
    checksumUnavailable: verificationDetails.checksumUnavailable,
  });
  await recordAssetAccessEvent({
    ctx,
    tenantId: auth.tenantId,
    assetId,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType: 'upload_confirm',
    visibility: asset.visibility,
    assetType: asset.assetType,
    storageProvider: asset.storageProvider,
    result: 'granted',
    metadata: {
      provider,
      bucket: asset.bucket,
      objectKey: asset.objectKey,
      publish,
      checksumVerified: verificationDetails.checksumVerified,
      checksumUnavailable: verificationDetails.checksumUnavailable,
    },
  });

  return {
    item,
    metadata,
    verification: verificationDetails,
    securityScan: {
      status: 'pending',
      provider: 'metadata_rules',
      requiredBeforePublish: true,
    },
  };
}

export async function signAssetUploadRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const body = await readJsonBody(ctx);
  const fileName = requiredString(body, 'fileName');
  const assetType = choice(body.assetType, ASSET_TYPES, 'document', 'assetType');
  const storageProvider = normalizeStorageProvider(nullableString(body.storageProvider), configuredDefaultStorageProvider());
  assertUploadProvider(storageProvider);
  const bucket = nullableString(body.bucket) || configuredDefaultStorageBucket();
  const objectKey =
    nullableString(body.objectKey) ||
    `${auth.tenantId}/${assetType}/${Date.now()}-${randomUUID()}-${safeFileName(fileName)}`;
  const expiresInSec = Math.min(Math.max(intValue(body.expiresInSec, 900), 60), 3600);
  const mimeType = validateMimeType(nullableString(body.mimeType) || 'application/octet-stream');
  const fileSizeBytes = body.fileSizeBytes === undefined ? null : validateFileSize(intValue(body.fileSizeBytes, 0));
  const cleanObjectKey = validateObjectKey(auth.tenantId, objectKey);
  const upload = await signStorageUpload({
    tenantId: auth.tenantId,
    provider: storageProvider,
    bucket,
    objectKey: cleanObjectKey,
    fileName,
    mimeType,
    fileSizeBytes,
    expiresInSec,
    upsert: body.upsert === true,
  });

  await recordAssetAudit(auth, 'content.asset.upload_signed', null, {
    provider: storageProvider,
    bucket,
    objectKey: cleanObjectKey,
    assetType,
    fileName,
    mimeType,
    fileSizeBytes,
  });
  await recordAssetAccessEvent({
    ctx,
    tenantId: auth.tenantId,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType: 'upload_sign',
    assetType,
    storageProvider,
    disposition: 'attachment',
    expiresInSec: upload.expiresInSec,
    signatureMode: upload.signatureMode,
    result: 'granted',
    metadata: {
      bucket,
      objectKey: cleanObjectKey,
      fileName,
      mimeType,
      fileSizeBytes,
      signature: signedAssetFingerprint(upload),
    },
  });

  return {
    upload,
    assetDraft: {
      assetType,
      storageProvider,
      bucket,
      objectKey: cleanObjectKey,
      fileName,
      mimeType,
      fileSizeBytes,
      checksumSha256: nullableString(body.checksumSha256),
    },
  };
}

export async function signAssetDownloadAdminRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const body = await readJsonBody(ctx);
  const assetId = requiredString(body, 'assetId');
  const requestedExpiresInSec = intValue(body.expiresInSec, 900);

  const asset = await queryOne<AssetRow>(
    `
    select id, tenant_id as "tenantId", asset_type as "assetType",
           storage_provider as "storageProvider", bucket, object_key as "objectKey",
           preview_object_key as "previewObjectKey",
           title, file_name as "fileName", cdn_url as "cdnUrl",
           preview_url as "previewUrl", mime_type as "mimeType",
           file_size_bytes as "fileSizeBytes", checksum_sha256 as "checksumSha256",
           visibility, status, upload_status as "uploadStatus",
           security_scan_status as "securityScanStatus",
           security_scan_provider as "securityScanProvider",
           security_scan_summary as "securityScanSummary",
           verified_size_bytes as "verifiedSizeBytes",
           verified_checksum_sha256 as "verifiedChecksumSha256",
           preview_status as "previewStatus",
           access_rules as "accessRules", metadata
    from public.content_assets
    where tenant_id = $1 and id = $2
    limit 1
    `,
    [auth.tenantId, assetId],
  );

  if (!asset) {
    throw new HttpError(404, 'Asset not found', 'ASSET_NOT_FOUND');
  }
  const expiresInSec = assetAccessTtl({
    actorRole: 'tenant_content_editor',
    accessType: 'admin_download',
    visibility: asset.visibility,
    assetType: asset.assetType,
    disposition: 'attachment',
    requestedExpiresInSec,
  });
  try {
    assertCdnAccessAllowed({
      assetId,
      visibility: asset.visibility,
      assetType: asset.assetType,
      storageProvider: asset.storageProvider as StorageProviderName,
      objectKey: asset.objectKey,
      cdnUrl: asset.cdnUrl,
      metadata: asset.metadata,
      accessRules: asset.accessRules,
    });
    assertAssetSecurityScanPassed({
      provider: asset.storageProvider as StorageProviderName,
      objectKey: asset.objectKey,
      securityScanStatus: asset.securityScanStatus,
    });
  } catch (error) {
    await recordDeniedAdminAssetAccess(ctx, auth, asset, 'admin_download', error);
    throw error;
  }

  await query(
    'update public.content_assets set download_count = download_count + 1, updated_at = now() where tenant_id = $1 and id = $2',
    [auth.tenantId, assetId],
  );

  const download = await signStorageDownload({
      tenantId: auth.tenantId,
      provider: asset.storageProvider as StorageProviderName,
      bucket: asset.bucket,
      objectKey: asset.objectKey,
      cdnUrl: asset.cdnUrl,
      fileName: asset.fileName,
      expiresInSec,
      disposition: 'attachment',
  });
  const watermark = buildAssetWatermarkContext({
    tenantId: auth.tenantId,
    assetId,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType: 'admin_download',
    visibility: asset.visibility,
    assetType: asset.assetType,
    fileName: asset.fileName,
    title: asset.title,
    expiresAt: download.expiresAt,
    metadata: asset.metadata,
  });
  await recordAssetAccessEvent({
    ctx,
    tenantId: auth.tenantId,
    assetId,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType: 'admin_download',
    visibility: asset.visibility,
    assetType: asset.assetType,
    storageProvider: asset.storageProvider,
    disposition: 'attachment',
    expiresInSec: download.expiresInSec,
    signatureMode: download.signatureMode,
    result: 'granted',
    metadata: { signature: signedAssetFingerprint(download), watermark },
  });

  return { item: asset, download, watermark };
}

export async function signAssetPreviewAdminRoute(ctx: RequestContext) {
  const auth = await requireTenantContentEditor(ctx);
  const body = await readJsonBody(ctx);
  const assetId = requiredString(body, 'assetId');
  const requestedExpiresInSec = intValue(body.expiresInSec, 900);

  const asset = await queryOne<AssetRow>(
    `
    select id, tenant_id as "tenantId", asset_type as "assetType",
           storage_provider as "storageProvider", bucket, object_key as "objectKey",
           preview_object_key as "previewObjectKey", title, file_name as "fileName",
           cdn_url as "cdnUrl", preview_url as "previewUrl", mime_type as "mimeType",
           file_size_bytes as "fileSizeBytes", checksum_sha256 as "checksumSha256",
           visibility, status, upload_status as "uploadStatus",
           security_scan_status as "securityScanStatus",
           security_scan_provider as "securityScanProvider",
           security_scan_summary as "securityScanSummary",
           verified_size_bytes as "verifiedSizeBytes",
           verified_checksum_sha256 as "verifiedChecksumSha256",
           preview_status as "previewStatus",
           access_rules as "accessRules", metadata
    from public.content_assets
    where tenant_id = $1 and id = $2
    limit 1
    `,
    [auth.tenantId, assetId],
  );

  if (!asset) throw new HttpError(404, 'Asset not found', 'ASSET_NOT_FOUND');
  if (!assetPreviewable(asset)) {
    throw new HttpError(400, 'Asset type does not support inline preview', 'ASSET_PREVIEW_NOT_SUPPORTED');
  }

  const objectKey = asset.previewObjectKey || asset.objectKey;
  const cdnUrl = asset.previewUrl || asset.cdnUrl;
  const fileName = asset.fileName || asset.title || 'preview.pdf';
  const expiresInSec = assetAccessTtl({
    actorRole: 'tenant_content_editor',
    accessType: 'admin_preview',
    visibility: asset.visibility,
    assetType: asset.assetType,
    disposition: 'inline',
    requestedExpiresInSec,
  });
  try {
    assertCdnAccessAllowed({
      assetId,
      visibility: asset.visibility,
      assetType: asset.assetType,
      storageProvider: asset.storageProvider as StorageProviderName,
      objectKey,
      cdnUrl,
      metadata: asset.metadata,
      accessRules: asset.accessRules,
    });
    assertAssetSecurityScanPassed({
      provider: asset.storageProvider as StorageProviderName,
      objectKey,
      securityScanStatus: asset.securityScanStatus,
    });
  } catch (error) {
    await recordDeniedAdminAssetAccess(ctx, auth, asset, 'admin_preview', error);
    throw error;
  }
  const preview = await signStorageDownload({
    tenantId: auth.tenantId,
    provider: asset.storageProvider as StorageProviderName,
    bucket: asset.bucket,
    objectKey,
    cdnUrl,
    fileName,
    expiresInSec,
    disposition: 'inline',
  });
  const watermark = buildAssetWatermarkContext({
    tenantId: auth.tenantId,
    assetId,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType: 'admin_preview',
    visibility: asset.visibility,
    assetType: asset.assetType,
    fileName,
    title: asset.title,
    expiresAt: preview.expiresAt,
    metadata: asset.metadata,
  });

  await recordAssetAccessEvent({
    ctx,
    tenantId: auth.tenantId,
    assetId,
    userId: auth.userId,
    actorRole: 'tenant_content_editor',
    accessType: 'admin_preview',
    visibility: asset.visibility,
    assetType: asset.assetType,
    storageProvider: asset.storageProvider,
    disposition: 'inline',
    expiresInSec: preview.expiresInSec,
    signatureMode: preview.signatureMode,
    result: 'granted',
    metadata: { signature: signedAssetFingerprint(preview), watermark },
  });

  return {
    item: {
      id: asset.id,
      assetType: asset.assetType,
      title: asset.title,
      fileName: asset.fileName,
      previewStatus: asset.previewStatus,
    },
    preview,
    watermark,
  };
}
