import crypto from 'node:crypto';
import { getHeader, HttpError, type RequestContext } from '../../core/http.js';
import { query } from '../../core/db.js';
import type {
  SignedStorageUrl,
  StorageContentDisposition,
  StorageProviderName,
} from './service.js';

export type AssetAccessType = 'download' | 'preview' | 'admin_download' | 'admin_preview' | 'upload_sign' | 'upload_confirm';
export type AssetActorRole = 'anonymous' | 'student' | 'tenant_admin' | 'tenant_content_editor' | 'system';
export type AssetAccessResult = 'granted' | 'denied';
export type AssetWatermarkMode = 'none' | 'visible_overlay';

export interface AssetWatermarkContextInput {
  tenantId: string;
  assetId?: string | null;
  userId?: string | null;
  actorRole: AssetActorRole;
  accessType: AssetAccessType | 'video_play';
  visibility?: string | null;
  assetType?: string | null;
  fileName?: string | null;
  title?: string | null;
  expiresAt?: string | null;
  metadata?: Record<string, unknown> | null;
}

export interface AssetWatermarkContext {
  mode: AssetWatermarkMode;
  required: boolean;
  text: string;
  traceId: string;
  position: 'diagonal' | 'bottom-right' | 'center';
  opacity: number;
  repeat: boolean;
  expiresAt: string | null;
  renderHint: string;
}

export interface AssetAccessRecordInput {
  ctx: RequestContext;
  tenantId: string;
  assetId?: string | null;
  userId?: string | null;
  actorRole: AssetActorRole;
  accessType: AssetAccessType;
  visibility?: string | null;
  assetType?: string | null;
  storageProvider?: string | null;
  disposition?: StorageContentDisposition | null;
  expiresInSec?: number | null;
  signatureMode?: string | null;
  result: AssetAccessResult;
  denyCode?: string | null;
  metadata?: Record<string, unknown>;
}

export interface AssetAccessPolicyInput {
  actorRole: AssetActorRole;
  accessType: AssetAccessType;
  visibility: string;
  assetType: string;
  disposition: StorageContentDisposition;
  requestedExpiresInSec?: number | null;
}

export interface AssetCdnPolicyInput {
  assetId: string;
  visibility: string;
  assetType: string;
  storageProvider: StorageProviderName;
  objectKey?: string | null;
  cdnUrl?: string | null;
  metadata?: Record<string, unknown> | null;
  accessRules?: Record<string, unknown> | null;
}

const LOCKED_VISIBILITIES = new Set(['members', 'svip', 'private']);
const SHORT_LIVED_ASSET_TYPES = new Set(['video', 'package']);
const WATERMARKED_ACCESS_TYPES = new Set(['preview', 'download', 'admin_preview', 'admin_download', 'video_play']);

function clientIpFrom(ctx: RequestContext) {
  return (getHeader(ctx.req, 'x-forwarded-for').split(',')[0] || getHeader(ctx.req, 'x-real-ip') || ctx.req.socket.remoteAddress || '').trim();
}

function userAgentFrom(ctx: RequestContext) {
  return getHeader(ctx.req, 'user-agent');
}

function objectValue(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function boolFlag(...values: unknown[]) {
  return values.some(value => value === true || value === 'true');
}

function stringValue(value: unknown) {
  return typeof value === 'string' && value.trim() ? value.trim() : '';
}

function safeWatermarkText(value: string, fallback: string) {
  const normalized = value.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim();
  return (normalized || fallback).slice(0, 48);
}

function numberValue(value: unknown, fallback: number, min: number, max: number) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.min(Math.max(parsed, min), max);
}

function watermarkTraceId(input: Pick<AssetWatermarkContextInput, 'tenantId' | 'assetId' | 'userId' | 'accessType' | 'expiresAt'>) {
  const seed = [
    input.tenantId,
    input.assetId || 'no-asset',
    input.userId || 'anonymous',
    input.accessType,
    input.expiresAt || new Date().toISOString(),
  ].join(':');
  return cryptoHash(seed).slice(0, 12).toUpperCase();
}

function cryptoHash(value: string) {
  return crypto.createHash('sha256').update(value).digest('hex');
}

function watermarkEnabledByMetadata(metadata: Record<string, unknown>) {
  if (metadata.watermark === false || metadata.watermark === 'false') return false;
  if (metadata.disableWatermark === true || metadata.disableWatermark === 'true') return false;
  return true;
}

export function buildAssetWatermarkContext(input: AssetWatermarkContextInput): AssetWatermarkContext {
  const metadata = objectValue(input.metadata);
  const watermarkConfig = objectValue(metadata.watermarkConfig);
  const requiredByVisibility = LOCKED_VISIBILITIES.has(input.visibility || '');
  const requiredByAssetType = SHORT_LIVED_ASSET_TYPES.has(input.assetType || '');
  const requiredByAccessType = WATERMARKED_ACCESS_TYPES.has(input.accessType);
  const canDisableByMetadata = !requiredByVisibility && !requiredByAssetType;
  const metadataEnabled = watermarkEnabledByMetadata(metadata) || !canDisableByMetadata;
  const enabled = metadataEnabled && requiredByAccessType && (requiredByVisibility || requiredByAssetType || input.actorRole !== 'anonymous');

  const traceId = watermarkTraceId(input);
  const defaultText = input.actorRole === 'tenant_content_editor' || input.actorRole === 'tenant_admin' ? '内部资料' : '仅限本人学习';
  const baseText = safeWatermarkText(
    stringValue(watermarkConfig.text) || stringValue(metadata.watermarkText),
    defaultText,
  );
  const identity = input.userId ? `账号:${cryptoHash(input.userId).slice(0, 8).toUpperCase()}` : '访客';
  const text = `${baseText} ${identity} ${traceId}`;

  if (!enabled) {
    return {
      mode: 'none',
      required: false,
      text: '',
      traceId,
      position: 'diagonal',
      opacity: 0,
      repeat: false,
      expiresAt: input.expiresAt || null,
      renderHint: 'none',
    };
  }

  const position = ['diagonal', 'bottom-right', 'center'].includes(stringValue(watermarkConfig.position))
    ? stringValue(watermarkConfig.position) as AssetWatermarkContext['position']
    : 'diagonal';

  return {
    mode: 'visible_overlay',
    required: true,
    text,
    traceId,
    position,
    opacity: numberValue(watermarkConfig.opacity, 0.16, 0.05, 0.35),
    repeat: watermarkConfig.repeat === false || watermarkConfig.repeat === 'false' ? false : true,
    expiresAt: input.expiresAt || null,
    renderHint: 'render_visible_overlay_before_opening_signed_url',
  };
}

export function assetAccessTtl(input: AssetAccessPolicyInput) {
  const requested = input.requestedExpiresInSec ?? 900;
  const minTtl = 60;
  let maxTtl = 900;

  if (input.actorRole === 'tenant_admin' || input.actorRole === 'tenant_content_editor') {
    maxTtl = input.accessType === 'admin_preview' ? 3600 : 86_400;
  } else if (input.disposition === 'inline') {
    maxTtl = 300;
  } else if (LOCKED_VISIBILITIES.has(input.visibility) || SHORT_LIVED_ASSET_TYPES.has(input.assetType)) {
    maxTtl = 300;
  }

  return Math.min(Math.max(requested, minTtl), maxTtl);
}

export function assertCdnAccessAllowed(input: AssetCdnPolicyInput) {
  if (!input.cdnUrl) return;

  const metadata = objectValue(input.metadata);
  const accessRules = objectValue(input.accessRules);
  const providerManagedAccess = boolFlag(
    metadata.providerManagedAccess,
    metadata.providerManagedCdn,
    metadata.cdnAccessMode === 'signed_by_provider',
    accessRules.providerManagedAccess,
    accessRules.providerManagedCdn,
    accessRules.cdnAccessMode === 'signed_by_provider',
  );

  if (LOCKED_VISIBILITIES.has(input.visibility) && !providerManagedAccess) {
    throw new HttpError(
      409,
      'Locked asset requires managed object signing or explicit provider-managed CDN access',
      'ASSET_CDN_ACCESS_NOT_ALLOWED',
    );
  }
}

export function signedAssetFingerprint(signed: SignedStorageUrl) {
  return {
    provider: signed.provider,
    bucket: signed.bucket,
    objectKey: signed.objectKey,
    method: signed.method,
    expiresAt: signed.expiresAt,
    expiresInSec: signed.expiresInSec,
    signatureMode: signed.signatureMode,
  };
}

export async function recordAssetAccessEvent(input: AssetAccessRecordInput) {
  await query(
    `
    insert into public.content_asset_access_events (
      tenant_id, asset_id, user_id, actor_role, access_type, visibility,
      asset_type, storage_provider, disposition, expires_in_sec,
      signature_mode, result, deny_code, ip_address, user_agent, metadata
    )
    values (
      $1, $2::uuid, $3::uuid, $4, $5, $6, $7, $8, $9, $10,
      $11, $12, $13, $14, $15, $16::jsonb
    )
    `,
    [
      input.tenantId,
      input.assetId || null,
      input.userId || null,
      input.actorRole,
      input.accessType,
      input.visibility || null,
      input.assetType || null,
      input.storageProvider || null,
      input.disposition || null,
      input.expiresInSec || null,
      input.signatureMode || null,
      input.result,
      input.denyCode || null,
      clientIpFrom(input.ctx) || null,
      userAgentFrom(input.ctx) || null,
      JSON.stringify(input.metadata || {}),
    ],
  );
}
