import crypto from 'node:crypto';
import type pg from 'pg';
import { getHeader, HttpError, type RequestContext } from '../../core/http.js';
import {
  intParam,
  optionalString,
  optionalStringArray,
  readJsonBody,
  requiredString,
  stringParam,
  tenantIdFrom,
  userIdFrom,
} from '../../core/request.js';
import { query, queryOne, transaction } from '../../core/db.js';
import { signStorageDownload, type StorageProviderName } from '../storage/service.js';
import {
  assetAccessTtl,
  assertCdnAccessAllowed,
  buildAssetWatermarkContext,
  signedAssetFingerprint,
} from '../storage/asset-access.js';
import { assertAssetSecurityScanPassed } from '../tenant-content/assets.js';

interface QuestionVideoRow {
  questionId: string;
  videoType: string;
  order: number;
  id: string;
  legacyId: string | null;
  title: string;
  description: string | null;
  videoUrl: string | null;
  thumbnailUrl: string | null;
  duration: number | null;
  knowledgeTags: unknown[];
  isGeneral: boolean;
  subjectId: string | null;
  difficulty: number | null;
  accessMode: string;
  freePreviewSeconds: number;
  playable: boolean;
}

interface VideoPlaybackRow {
  id: string;
  title: string;
  description: string | null;
  videoUrl: string | null;
  thumbnailUrl: string | null;
  duration: number | null;
  subjectId: string | null;
  regionId: string | null;
  accessMode: string;
  freePreviewSeconds: number;
  assetId: string | null;
  storageProvider: string | null;
  bucket: string | null;
  objectKey: string | null;
  cdnUrl: string | null;
  fileName: string | null;
  assetTitle: string | null;
  assetStatus: string | null;
  assetType: string | null;
  assetVisibility: string | null;
  assetUploadStatus: string | null;
  assetSecurityScanStatus: string | null;
  assetAccessRules: Record<string, unknown> | null;
  assetMetadata: Record<string, unknown> | null;
}

interface EntitlementAccessRow {
  id: string;
}

interface QuotaAccountRow {
  id: string;
  total_quota: number;
  used_quota: number;
}

interface VideoPlayEventRow {
  id: string;
  status: 'issued' | 'started' | 'completed' | 'expired' | 'revoked';
  metadata: Record<string, unknown>;
}

function videoSelectSql() {
  return `
    select qv.question_id as "questionId", qv.video_type as "videoType",
           qv.sort_order as "order",
           v.id, v.legacy_id as "legacyId", v.title, v.description,
           case when v.access_mode = 'free' then v.video_url else null end as "videoUrl",
           v.thumbnail_url as "thumbnailUrl",
           v.duration_seconds as "duration", v.knowledge_tags as "knowledgeTags",
           v.is_general as "isGeneral", v.subject_id as "subjectId",
           v.difficulty, v.access_mode as "accessMode",
           v.free_preview_seconds as "freePreviewSeconds",
           (v.access_mode = 'free') as playable
    from public.question_videos qv
    join public.video_explanations v on v.id = qv.video_id and v.tenant_id = qv.tenant_id
  `;
}

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 createPlayToken() {
  return `vp_${crypto.randomBytes(32).toString('base64url')}`;
}

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

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

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

function progressEventType(value: unknown) {
  const eventType = typeof value === 'string' ? value.trim() : '';
  if (eventType === 'start' || eventType === 'heartbeat' || eventType === 'complete') return eventType;
  throw new HttpError(400, 'eventType must be start, heartbeat, or complete', 'INVALID_VIDEO_PROGRESS_EVENT');
}

async function activeSvipEntitlement(tenantId: string, userId: string, video: VideoPlaybackRow) {
  const now = new Date().toISOString();
  return queryOne<EntitlementAccessRow>(
    `
    select id
    from public.entitlements
    where tenant_id = $1
      and user_id = $2
      and entitlement_type = 'svip'
      and status = 'active'
      and starts_at <= $3::timestamptz
      and (expires_at is null or expires_at > $3::timestamptz)
      and (
        scope_type = 'tenant'
        or ($4::uuid is not null and scope_type = 'region' and scope_id = $4::uuid)
        or ($5::uuid is not null and scope_type = 'subject' and scope_id = $5::uuid)
      )
    order by case when scope_type = 'subject' then 0 when scope_type = 'region' then 1 else 2 end,
             expires_at desc nulls first
    limit 1
    `,
    [tenantId, userId, now, video.regionId, video.subjectId],
  );
}

async function availableVideoQuotaAccount(client: pg.PoolClient, tenantId: string, userId: string) {
  const result = await client.query<QuotaAccountRow>(
    `
    select id, total_quota, used_quota
    from public.video_play_quota_accounts
    where tenant_id = $1
      and user_id = $2
      and quota_type = 'video_play'
      and total_quota > used_quota
      and (expires_at is null or expires_at > now())
    order by expires_at asc nulls last, created_at asc
    limit 1
    for update
    `,
    [tenantId, userId],
  );
  return result.rows[0] || null;
}

function isExternalPublicVideo(video: VideoPlaybackRow) {
  return video.videoUrl && /^https?:\/\//i.test(video.videoUrl);
}

async function signVideoPlayback(tenantId: string, video: VideoPlaybackRow) {
  if (video.assetId) {
    if (video.assetStatus !== 'active') {
      throw new HttpError(404, 'Video asset is not active', 'VIDEO_ASSET_NOT_FOUND');
    }
    if (video.objectKey && video.assetUploadStatus !== 'verified') {
      throw new HttpError(409, 'Video asset upload has not been verified', 'VIDEO_ASSET_UPLOAD_NOT_VERIFIED');
    }
    assertAssetSecurityScanPassed({
      provider: video.storageProvider as StorageProviderName,
      objectKey: video.objectKey,
      securityScanStatus: video.assetSecurityScanStatus,
    });
    assertCdnAccessAllowed({
      assetId: video.assetId,
      visibility: video.assetVisibility || 'svip',
      assetType: video.assetType || 'video',
      storageProvider: video.storageProvider as StorageProviderName,
      objectKey: video.objectKey,
      cdnUrl: video.cdnUrl,
      metadata: video.assetMetadata,
      accessRules: video.assetAccessRules,
    });
    return signStorageDownload({
      tenantId,
      provider: video.storageProvider as StorageProviderName,
      bucket: video.bucket,
      objectKey: video.objectKey,
      cdnUrl: video.cdnUrl,
      fileName: video.fileName || video.assetTitle || video.title,
      expiresInSec: assetAccessTtl({
        actorRole: 'student',
        accessType: 'download',
        visibility: video.assetVisibility || 'svip',
        assetType: video.assetType || 'video',
        disposition: 'attachment',
        requestedExpiresInSec: 600,
      }),
    });
  }
  if (isExternalPublicVideo(video)) {
    return signStorageDownload({
      tenantId,
      provider: 'external_url',
      bucket: null,
      objectKey: null,
      cdnUrl: video.videoUrl,
      fileName: video.title,
      expiresInSec: 600,
    });
  }
  throw new HttpError(409, 'Video does not have a playable asset', 'VIDEO_ASSET_REQUIRED');
}

export async function questionVideosRoute(ctx: RequestContext) {
  const tenantId = await tenantIdFrom(ctx);
  const questionId = stringParam(ctx, 'questionId');
  if (!questionId) {
    throw new HttpError(400, 'questionId is required', 'QUESTION_ID_REQUIRED');
  }

  const videos = await query<QuestionVideoRow>(
    `
    ${videoSelectSql()}
    where qv.tenant_id = $1 and qv.question_id = $2 and v.is_active = true
    order by qv.sort_order asc, v.sort_order asc, v.created_at asc
    `,
    [tenantId, questionId],
  );

  return { videos, total: videos.length };
}

export async function questionVideosBatchRoute(ctx: RequestContext) {
  const body = await readJsonBody(ctx);
  const tenantId = await tenantIdFrom(ctx);
  const questionIds = optionalStringArray(body, 'questionIds').slice(0, 50);

  if (!questionIds.length) {
    throw new HttpError(400, 'questionIds is required', 'QUESTION_IDS_REQUIRED');
  }

  const rows = await query<QuestionVideoRow>(
    `
    ${videoSelectSql()}
    where qv.tenant_id = $1 and qv.question_id = any($2::uuid[]) and v.is_active = true
    order by qv.question_id, qv.sort_order asc, v.sort_order asc
    `,
    [tenantId, questionIds],
  );

  const data: Record<string, { hasVideo: boolean; videos: QuestionVideoRow[] }> = {};
  for (const row of rows) {
    data[row.questionId] ||= { hasVideo: true, videos: [] };
    data[row.questionId].videos.push(row);
  }

  return { data };
}

export async function videoSearchRoute(ctx: RequestContext) {
  const tenantId = await tenantIdFrom(ctx);
  const subjectId = stringParam(ctx, 'subjectId');
  const tags = stringParam(ctx, 'tags')
    .split(',')
    .map(tag => tag.trim())
    .filter(Boolean);
  const limit = intParam(ctx, 'limit', 50, 200);

  const videos = await query(
    `
    select id, legacy_id as "legacyId", title, description,
           case when access_mode = 'free' then video_url else null end as "videoUrl",
           thumbnail_url as "thumbnailUrl",
           duration_seconds as "duration", knowledge_tags as "knowledgeTags",
           is_general as "isGeneral", subject_id as "subjectId",
           difficulty, access_mode as "accessMode",
           free_preview_seconds as "freePreviewSeconds",
           (access_mode = 'free') as playable,
           sort_order as "order", created_at as "createdAt"
    from public.video_explanations
    where tenant_id = $1
      and is_active = true
      and is_general = true
      and ($2::uuid is null or subject_id = $2::uuid)
      and ($3::text[] = '{}'::text[] or knowledge_tags ?| $3::text[])
    order by sort_order asc, created_at desc
    limit $4
    `,
    [tenantId, subjectId || null, tags, limit],
  );

  return { videos, total: videos.length };
}

export async function videoPlaybackRoute(ctx: RequestContext) {
  const tenantId = await tenantIdFrom(ctx);
  const body = await readJsonBody(ctx);
  const userId = await userIdFrom(ctx, body);
  const videoId = optionalString(body, 'videoId') || stringParam(ctx, 'videoId');
  const questionId = optionalString(body, 'questionId') || null;
  if (!videoId) throw new HttpError(400, 'videoId is required', 'VIDEO_ID_REQUIRED');

  const video = await queryOne<VideoPlaybackRow>(
    `
    select v.id, v.title, v.description, v.video_url as "videoUrl",
           v.thumbnail_url as "thumbnailUrl", v.duration_seconds as "duration",
           v.subject_id as "subjectId",
           coalesce(qb.region_id, qs.region_id, vs.region_id) as "regionId",
           v.access_mode as "accessMode",
           v.free_preview_seconds as "freePreviewSeconds",
           v.asset_id as "assetId",
           a.storage_provider as "storageProvider", a.bucket, a.object_key as "objectKey",
           a.cdn_url as "cdnUrl", a.file_name as "fileName",
           a.title as "assetTitle", a.status as "assetStatus",
           a.asset_type as "assetType", a.visibility as "assetVisibility",
           a.upload_status as "assetUploadStatus",
           a.security_scan_status as "assetSecurityScanStatus",
           a.access_rules as "assetAccessRules", a.metadata as "assetMetadata"
    from public.video_explanations v
    left join public.content_assets a on a.tenant_id = v.tenant_id and a.id = v.asset_id
    left join public.subjects vs on vs.tenant_id = v.tenant_id and vs.id = v.subject_id
    left join public.questions q on q.tenant_id = v.tenant_id and q.id = $3::uuid
    left join public.subjects qs on qs.tenant_id = q.tenant_id and qs.id = q.subject_id
    left join public.question_banks qb on qb.tenant_id = q.tenant_id and qb.id = q.question_bank_id
    where v.tenant_id = $1
      and v.id = $2
      and v.is_active = true
      and (
        $3::uuid is null
        or exists (
          select 1
          from public.question_videos qv
          where qv.tenant_id = v.tenant_id
            and qv.video_id = v.id
            and qv.question_id = $3::uuid
        )
      )
    limit 1
    `,
    [tenantId, videoId, questionId],
  );

  if (!video) throw new HttpError(404, 'Video not found', 'VIDEO_NOT_FOUND');

  const playback = await transaction(async client => {
    let entitlementId: string | null = null;
    let quotaAccountId: string | null = null;
    let consumedQuota = 0;
    const accessMode = video.accessMode || 'svip';

    if (accessMode === 'svip') {
      const entitlement = await activeSvipEntitlement(tenantId, userId, video);
      if (!entitlement) throw new HttpError(403, 'SVIP entitlement is required for this video', 'VIDEO_SVIP_REQUIRED');
      entitlementId = entitlement.id;
    } else if (accessMode === 'video_quota') {
      const account = await availableVideoQuotaAccount(client, tenantId, userId);
      if (!account) throw new HttpError(403, 'Video play quota is required', 'VIDEO_QUOTA_REQUIRED');
      await client.query(
        `
        update public.video_play_quota_accounts
        set used_quota = used_quota + 1, updated_at = now()
        where tenant_id = $1 and id = $2
        `,
        [tenantId, account.id],
      );
      quotaAccountId = account.id;
      consumedQuota = 1;
    }

    const signed = await signVideoPlayback(tenantId, video);
    const watermark = buildAssetWatermarkContext({
      tenantId,
      assetId: video.assetId,
      userId,
      actorRole: 'student',
      accessType: 'video_play',
      visibility: video.assetVisibility || 'svip',
      assetType: video.assetType || 'video',
      fileName: video.fileName,
      title: video.assetTitle || video.title,
      expiresAt: signed.expiresAt,
      metadata: video.assetMetadata,
    });
    const token = createPlayToken();
    const tokenHash = hashPlayToken(token);

    const event = await client.query(
      `
      insert into public.video_play_events (
        tenant_id, user_id, video_id, question_id, entitlement_id, quota_account_id,
        play_token_hash, status, access_mode, consumed_quota,
        signed_url_expires_at, ip_address, user_agent, metadata
      )
      values ($1, $2, $3, $4::uuid, $5::uuid, $6::uuid, $7, 'issued', $8, $9,
              $10::timestamptz, $11, $12, $13::jsonb)
      returning id, created_at as "createdAt"
      `,
      [
        tenantId,
        userId,
        video.id,
        questionId,
        entitlementId,
        quotaAccountId,
        tokenHash,
        accessMode,
        consumedQuota,
        signed.expiresAt,
        clientIpFrom(ctx) || null,
        userAgentFrom(ctx) || null,
        JSON.stringify({
          signatureMode: signed.signatureMode,
          provider: signed.provider,
          bucket: signed.bucket,
          objectKey: signed.objectKey,
          assetId: video.assetId,
          freePreviewSeconds: video.freePreviewSeconds,
          signature: signedAssetFingerprint(signed),
          watermark,
        }),
      ],
    );

    await client.query(
      'update public.video_explanations set play_count = play_count + 1, updated_at = now() where tenant_id = $1 and id = $2',
      [tenantId, video.id],
    );

    return {
      playEvent: event.rows[0],
      playToken: token,
      playback: signed,
      watermark,
      access: {
        mode: accessMode,
        entitlementId,
        quotaAccountId,
        consumedQuota,
      },
    };
  });

  return {
    item: {
      id: video.id,
      title: video.title,
      description: video.description,
      thumbnailUrl: video.thumbnailUrl,
      duration: video.duration,
      accessMode: video.accessMode,
      freePreviewSeconds: video.freePreviewSeconds,
    },
    ...playback,
  };
}

export async function videoProgressRoute(ctx: RequestContext) {
  const tenantId = await tenantIdFrom(ctx);
  const body = await readJsonBody(ctx);
  const userId = await userIdFrom(ctx);
  const playToken = requiredString(body, 'playToken');
  if (!playToken.startsWith('vp_') || playToken.length < 24 || playToken.length > 160) {
    throw new HttpError(400, 'Invalid play token', 'INVALID_PLAY_TOKEN');
  }

  const eventType = progressEventType(body.eventType);
  const progressSeconds = boundedInt(body.progressSeconds, 0, 0, 86_400);
  const durationSeconds = boundedInt(body.durationSeconds, 0, 0, 86_400);
  const watchedSecondsInput = boundedInt(body.watchedSeconds ?? progressSeconds, progressSeconds, 0, 86_400);
  const completedByProgress = durationSeconds > 0 && progressSeconds >= Math.floor(durationSeconds * 0.9);
  const nextStatus = eventType === 'complete' || completedByProgress ? 'completed' : 'started';
  const tokenHash = hashPlayToken(playToken);

  const item = await transaction(async client => {
    const currentResult = await client.query<VideoPlayEventRow>(
      `
      select id, status, metadata
      from public.video_play_events
      where tenant_id = $1
        and user_id = $2
        and play_token_hash = $3
      limit 1
      for update
      `,
      [tenantId, userId, tokenHash],
    );
    const current = currentResult.rows[0];
    if (!current) throw new HttpError(404, 'Video play event not found', 'VIDEO_PLAY_EVENT_NOT_FOUND');
    if (current.status === 'revoked') {
      throw new HttpError(409, 'Video play event has been revoked', 'VIDEO_PLAY_EVENT_REVOKED');
    }
    if (current.status === 'expired') {
      throw new HttpError(409, 'Video play event has expired', 'VIDEO_PLAY_EVENT_EXPIRED');
    }

    const nowIso = new Date().toISOString();
    const metadata = objectValue(current.metadata);
    const previousPlayback = objectValue(metadata.playback);
    const previousWatched = boundedInt(previousPlayback.watchedSeconds, 0, 0, 86_400);
    const watchedSeconds = Math.max(previousWatched, watchedSecondsInput, progressSeconds);
    const completedAt = nextStatus === 'completed'
      ? String(previousPlayback.completedAt || nowIso)
      : (typeof previousPlayback.completedAt === 'string' ? previousPlayback.completedAt : null);
    const startedAt = typeof previousPlayback.startedAt === 'string' ? previousPlayback.startedAt : nowIso;
    const finalStatus = current.status === 'completed' ? 'completed' : nextStatus;
    const completionRate = durationSeconds > 0 ? Number(Math.min(progressSeconds / durationSeconds, 1).toFixed(4)) : null;

    const nextMetadata = {
      ...metadata,
      playback: {
        ...previousPlayback,
        startedAt,
        completedAt,
        lastEventType: eventType,
        lastProgressSeconds: progressSeconds,
        durationSeconds: durationSeconds || previousPlayback.durationSeconds || null,
        watchedSeconds,
        completionRate,
        updatedAt: nowIso,
      },
    };

    const updated = await client.query(
      `
      update public.video_play_events
      set status = $4,
          metadata = $5::jsonb,
          updated_at = now()
      where tenant_id = $1
        and user_id = $2
        and play_token_hash = $3
      returning id, video_id as "videoId", question_id as "questionId",
                status, access_mode as "accessMode",
                consumed_quota as "consumedQuota",
                metadata -> 'playback' as playback,
                updated_at as "updatedAt"
      `,
      [tenantId, userId, tokenHash, finalStatus, JSON.stringify(nextMetadata)],
    );
    return updated.rows[0];
  });

  return { item };
}
