import type pg from 'pg';
import { HttpError } from '../../core/http.js';

type JsonObject = Record<string, unknown>;

export interface PracticeAssemblyAccessInput {
  mode: string;
  blueprintId: string | null;
  collectionId: string | null;
  entryId: string | null;
  contentNodeId: string | null;
  rules: JsonObject;
}

export interface PracticeAccessDecision {
  accessMode: 'free' | 'svip' | 'staff';
  entitlementId: string | null;
  questionIds: string[];
  consumedFreeQuota: number;
  accessSnapshot: JsonObject;
}

interface PracticeAccessScopeRow {
  entryId: string | null;
  entryRegionId: string | null;
  entryVisibility: string | null;
  entryAccessRules: JsonObject | null;
  nodeId: string | null;
  nodeRegionId: string | null;
  nodeAccessRules: JsonObject | null;
  nodeMetadata: JsonObject | null;
  collectionId: string | null;
  collectionRegionId: string | null;
  collectionSubjectId: string | null;
  collectionQuestionBankId: string | null;
  collectionAccessRules: JsonObject | null;
  collectionMetadata: JsonObject | null;
  blueprintId: string | null;
  blueprintRegionId: string | null;
  blueprintAccessRules: JsonObject | null;
  blueprintRules: JsonObject | null;
}

interface QuestionScopeRow {
  id: string;
  regionId: string | null;
  subjectId: string | null;
  questionBankId: string | null;
  contentNodeId: string | null;
  primaryCollectionId: string | null;
}

interface EntitlementRow {
  id: string;
  scopeType: string;
  scopeId: string | null;
  expiresAt: string | null;
}

interface StaffRow {
  id: string;
  role: string;
}

function isObject(value: unknown): value is JsonObject {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function objectValue(value: unknown): JsonObject {
  return isObject(value) ? value : {};
}

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

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

function boolValue(value: unknown, fallback = false) {
  return typeof value === 'boolean' ? value : fallback;
}

function mergeRules(...rules: unknown[]): JsonObject {
  return rules.reduce<JsonObject>((acc, rule) => ({ ...acc, ...objectValue(rule) }), {});
}

function resolveAccessMode(scope: PracticeAccessScopeRow, assembly: PracticeAssemblyAccessInput, mergedRules: JsonObject) {
  const explicitMode = stringValue(mergedRules.accessMode || mergedRules.mode || mergedRules.requiredAccess);
  if (['public', 'free'].includes(explicitMode)) return 'free';
  if (['members', 'svip', 'paid'].includes(explicitMode)) return 'svip';
  if (scope.entryVisibility === 'svip') return 'svip';
  if (scope.entryVisibility === 'members') return 'free';
  if (assembly.mode === 'mock_exam' && boolValue(mergedRules.mockExamRequiresSvip, false)) return 'svip';
  return 'free';
}

function scopeCandidates(input: {
  tenantId: string;
  mergedRules: JsonObject;
  scope: PracticeAccessScopeRow;
  questions: QuestionScopeRow[];
}) {
  const regionIds = new Set<string>();
  const subjectIds = new Set<string>();
  const questionBankIds = new Set<string>();

  for (const id of [input.scope.blueprintRegionId, input.scope.collectionRegionId, input.scope.nodeRegionId, input.scope.entryRegionId]) {
    if (id) regionIds.add(id);
  }
  for (const question of input.questions) {
    if (question.regionId) regionIds.add(question.regionId);
    if (question.subjectId) subjectIds.add(question.subjectId);
    if (question.questionBankId) questionBankIds.add(question.questionBankId);
  }

  const requiredRegionId = stringValue(input.mergedRules.regionId || input.mergedRules.requiredRegionId);
  const requiredSubjectId = stringValue(input.mergedRules.subjectId || input.mergedRules.requiredSubjectId);
  const requiredQuestionBankId = stringValue(input.mergedRules.questionBankId || input.mergedRules.requiredQuestionBankId);
  if (requiredRegionId) regionIds.add(requiredRegionId);
  if (requiredSubjectId) subjectIds.add(requiredSubjectId);
  if (requiredQuestionBankId) questionBankIds.add(requiredQuestionBankId);
  if (input.scope.collectionSubjectId) subjectIds.add(input.scope.collectionSubjectId);
  if (input.scope.collectionQuestionBankId) questionBankIds.add(input.scope.collectionQuestionBankId);

  return {
    tenantIds: [input.tenantId],
    regionIds: [...regionIds],
    subjectIds: [...subjectIds],
    questionBankIds: [...questionBankIds],
  };
}

function hasScopeAccess(entitlement: EntitlementRow, candidates: ReturnType<typeof scopeCandidates>) {
  if (entitlement.scopeType === 'tenant') return true;
  if (!entitlement.scopeId) return false;
  if (entitlement.scopeType === 'region') return candidates.regionIds.includes(entitlement.scopeId);
  if (entitlement.scopeType === 'subject') return candidates.subjectIds.includes(entitlement.scopeId);
  if (entitlement.scopeType === 'question_bank') return candidates.questionBankIds.includes(entitlement.scopeId);
  return false;
}

async function loadScope(client: pg.PoolClient, tenantId: string, assembly: PracticeAssemblyAccessInput) {
  const result = await client.query<PracticeAccessScopeRow>(
    `
    select
      ce.id as "entryId",
      ce.region_id as "entryRegionId",
      ce.visibility as "entryVisibility",
      ce.access_rules as "entryAccessRules",
      cn.id as "nodeId",
      cn.region_id as "nodeRegionId",
      cn.access_rules as "nodeAccessRules",
      cn.metadata as "nodeMetadata",
      qc.id as "collectionId",
      qc.region_id as "collectionRegionId",
      qc.subject_id as "collectionSubjectId",
      qc.question_bank_id as "collectionQuestionBankId",
      qc.access_rules as "collectionAccessRules",
      qc.metadata as "collectionMetadata",
      pb.id as "blueprintId",
      pb.region_id as "blueprintRegionId",
      pb.access_rules as "blueprintAccessRules",
      pb.rules as "blueprintRules"
    from (select $1::uuid as tenant_id) t
    left join public.content_entries ce on ce.tenant_id = t.tenant_id and ce.id = $2::uuid
    left join public.content_nodes cn on cn.tenant_id = t.tenant_id and cn.id = $3::uuid
    left join public.question_collections qc on qc.tenant_id = t.tenant_id and qc.id = $4::uuid
    left join public.practice_blueprints pb on pb.tenant_id = t.tenant_id and pb.id = $5::uuid
    limit 1
    `,
    [tenantId, assembly.entryId, assembly.contentNodeId, assembly.collectionId, assembly.blueprintId],
  );
  return result.rows[0] || {
    entryId: null,
    entryRegionId: null,
    entryVisibility: null,
    entryAccessRules: null,
    nodeId: null,
    nodeRegionId: null,
    nodeAccessRules: null,
    nodeMetadata: null,
    collectionId: null,
    collectionRegionId: null,
    collectionSubjectId: null,
    collectionQuestionBankId: null,
    collectionAccessRules: null,
    collectionMetadata: null,
    blueprintId: null,
    blueprintRegionId: null,
    blueprintAccessRules: null,
    blueprintRules: null,
  };
}

async function loadQuestionScopes(client: pg.PoolClient, tenantId: string, questionIds: string[]) {
  if (!questionIds.length) return [];
  const result = await client.query<QuestionScopeRow>(
    `
    select q.id,
           coalesce(qb.region_id, s.region_id) as "regionId",
           q.subject_id as "subjectId",
           q.question_bank_id as "questionBankId",
           q.content_node_id as "contentNodeId",
           q.primary_collection_id as "primaryCollectionId"
    from public.questions q
    left join public.subjects s on s.tenant_id = q.tenant_id and s.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 q.tenant_id = $1 and q.id = any($2::uuid[])
    `,
    [tenantId, questionIds],
  );
  const order = new Map(questionIds.map((id, index) => [id, index]));
  return result.rows.sort((left, right) => (order.get(left.id) ?? 0) - (order.get(right.id) ?? 0));
}

async function loadActiveEntitlements(client: pg.PoolClient, tenantId: string, userId: string) {
  const result = await client.query<EntitlementRow>(
    `
    select id, scope_type as "scopeType", scope_id as "scopeId", expires_at as "expiresAt"
    from public.entitlements
    where tenant_id = $1
      and user_id = $2
      and entitlement_type = 'svip'
      and status = 'active'
      and starts_at <= now()
      and (expires_at is null or expires_at > now())
    order by case scope_type
      when 'subject' then 0
      when 'question_bank' then 1
      when 'region' then 2
      else 3
    end, expires_at desc nulls first
    `,
    [tenantId, userId],
  );
  return result.rows;
}

async function loadStaffMembership(client: pg.PoolClient, tenantId: string, userId: string) {
  const result = await client.query<StaffRow>(
    `
    select id, role
    from public.tenant_memberships
    where tenant_id = $1
      and user_id = $2
      and status = 'active'
      and role in ('tenant_owner', 'tenant_admin', 'tenant_operator', 'teacher')
    order by case role
      when 'tenant_owner' then 0
      when 'tenant_admin' then 1
      when 'tenant_operator' then 2
      else 3
    end
    limit 1
    `,
    [tenantId, userId],
  );
  return result.rows[0] || null;
}

async function consumeDailyFreeQuota(client: pg.PoolClient, input: {
  tenantId: string;
  userId: string;
  requestedCount: number;
  dailyLimit: number;
  scopeType: string;
  scopeId: string | null;
  metadata: JsonObject;
}) {
  if (input.dailyLimit <= 0) return { grantedCount: 0, consumedFreeQuota: 0, remainingBefore: 0, remainingAfter: 0 };

  const usage = await client.query<{ used_count: number }>(
    `
    insert into public.practice_daily_usage (
      tenant_id, user_id, usage_date, scope_type, scope_id, free_limit, used_count, metadata
    )
    values ($1, $2, current_date, $3, $4::uuid, $5, 0, $6::jsonb)
    on conflict (tenant_id, user_id, usage_date, scope_type, scope_id)
    do update set free_limit = excluded.free_limit,
                  metadata = public.practice_daily_usage.metadata || excluded.metadata,
                  updated_at = now()
    returning used_count
    `,
    [
      input.tenantId,
      input.userId,
      input.scopeType,
      input.scopeId,
      input.dailyLimit,
      JSON.stringify(input.metadata),
    ],
  );

  const usedCount = Number(usage.rows[0]?.used_count || 0);
  const remainingBefore = Math.max(0, input.dailyLimit - usedCount);
  const grantedCount = Math.min(input.requestedCount, remainingBefore);
  if (grantedCount > 0) {
    await client.query(
      `
      update public.practice_daily_usage
      set used_count = used_count + $5,
          updated_at = now()
      where tenant_id = $1
        and user_id = $2
        and usage_date = current_date
        and scope_type = $3
        and (($4::uuid is null and scope_id is null) or scope_id = $4::uuid)
        and used_count + $5 <= free_limit
      `,
      [input.tenantId, input.userId, input.scopeType, input.scopeId, grantedCount],
    );
  }

  return {
    grantedCount,
    consumedFreeQuota: grantedCount,
    remainingBefore,
    remainingAfter: Math.max(0, remainingBefore - grantedCount),
  };
}

export async function authorizePracticeSession(client: pg.PoolClient, input: {
  tenantId: string;
  userId: string;
  questionIds: string[];
  assembly: PracticeAssemblyAccessInput;
}): Promise<PracticeAccessDecision> {
  const scope = await loadScope(client, input.tenantId, input.assembly);
  const questionScopes = await loadQuestionScopes(client, input.tenantId, input.questionIds);
  const mergedRules = mergeRules(
    scope.entryAccessRules,
    scope.nodeMetadata?.accessRules,
    scope.nodeAccessRules,
    scope.collectionMetadata?.accessRules,
    scope.collectionAccessRules,
    scope.blueprintRules?.accessRules,
    scope.blueprintAccessRules,
    input.assembly.rules?.accessRules,
  );
  const requiredAccess = resolveAccessMode(scope, input.assembly, mergedRules);
  const dailyLimit = positiveIntValue(
    mergedRules.freeDailyLimit ?? mergedRules.dailyFreeLimit ?? mergedRules.freeQuestionLimit,
    25,
    500,
  );
  const maxFreePerSession = positiveIntValue(
    mergedRules.freeSessionLimit ?? mergedRules.maxFreePerSession,
    dailyLimit,
    dailyLimit,
  );
  const candidates = scopeCandidates({
    tenantId: input.tenantId,
    mergedRules,
    scope,
    questions: questionScopes,
  });
  const staff = await loadStaffMembership(client, input.tenantId, input.userId);

  if (staff && boolValue(mergedRules.staffBypass, true)) {
    return {
      accessMode: 'staff',
      entitlementId: null,
      questionIds: input.questionIds,
      consumedFreeQuota: 0,
      accessSnapshot: {
        requiredAccess,
        grantedBy: 'staff',
        staffRole: staff.role,
        requestedCount: input.questionIds.length,
        grantedCount: input.questionIds.length,
        scope: candidates,
        rules: mergedRules,
      },
    };
  }

  const entitlement = (await loadActiveEntitlements(client, input.tenantId, input.userId))
    .find(row => hasScopeAccess(row, candidates));
  if (entitlement) {
    return {
      accessMode: 'svip',
      entitlementId: entitlement.id,
      questionIds: input.questionIds,
      consumedFreeQuota: 0,
      accessSnapshot: {
        requiredAccess,
        grantedBy: 'svip',
        requestedCount: input.questionIds.length,
        grantedCount: input.questionIds.length,
        entitlement: {
          id: entitlement.id,
          scopeType: entitlement.scopeType,
          scopeId: entitlement.scopeId,
          expiresAt: entitlement.expiresAt,
        },
        scope: candidates,
        rules: mergedRules,
      },
    };
  }

  if (requiredAccess === 'svip') {
    await recordPracticeAccessEvent(client, {
      tenantId: input.tenantId,
      userId: input.userId,
      eventType: 'session_denied',
      accessMode: 'denied',
      requestedCount: input.questionIds.length,
      grantedCount: 0,
      consumedFreeQuota: 0,
      reason: 'SVIP_REQUIRED',
      scopeType: 'tenant',
      scopeId: null,
      entitlementId: null,
      metadata: { requiredAccess, scope: candidates, rules: mergedRules },
    });
    throw new HttpError(403, 'SVIP entitlement is required for this practice target', 'PRACTICE_SVIP_REQUIRED');
  }

  const quotaScopeType = stringValue(mergedRules.freeQuotaScopeType) || 'tenant';
  const quotaScopeId =
    quotaScopeType === 'region'
      ? candidates.regionIds[0] || null
      : quotaScopeType === 'content_entry'
        ? input.assembly.entryId
        : quotaScopeType === 'content_node'
          ? input.assembly.contentNodeId
          : quotaScopeType === 'collection'
            ? input.assembly.collectionId
            : quotaScopeType === 'blueprint'
              ? input.assembly.blueprintId
              : null;
  const cappedRequestCount = Math.min(input.questionIds.length, maxFreePerSession);
  const quota = await consumeDailyFreeQuota(client, {
    tenantId: input.tenantId,
    userId: input.userId,
    requestedCount: cappedRequestCount,
    dailyLimit,
    scopeType: quotaScopeType,
    scopeId: quotaScopeId,
    metadata: {
      source: 'practice_session',
      mode: input.assembly.mode,
      blueprintId: input.assembly.blueprintId,
      collectionId: input.assembly.collectionId,
      entryId: input.assembly.entryId,
      contentNodeId: input.assembly.contentNodeId,
    },
  });

  if (quota.grantedCount <= 0) {
    await recordPracticeAccessEvent(client, {
      tenantId: input.tenantId,
      userId: input.userId,
      eventType: 'session_denied',
      accessMode: 'denied',
      requestedCount: input.questionIds.length,
      grantedCount: 0,
      consumedFreeQuota: 0,
      reason: 'FREE_DAILY_LIMIT_REACHED',
      scopeType: quotaScopeType,
      scopeId: quotaScopeId,
      entitlementId: null,
      metadata: { requiredAccess, dailyLimit, scope: candidates, rules: mergedRules, quota },
    });
    throw new HttpError(403, 'Daily free practice quota is used up', 'PRACTICE_FREE_LIMIT_REACHED');
  }

  const grantedQuestionIds = input.questionIds.slice(0, quota.grantedCount);
  return {
    accessMode: 'free',
    entitlementId: null,
    questionIds: grantedQuestionIds,
    consumedFreeQuota: quota.consumedFreeQuota,
    accessSnapshot: {
      requiredAccess,
      grantedBy: 'free_quota',
      requestedCount: input.questionIds.length,
      grantedCount: grantedQuestionIds.length,
      truncated: grantedQuestionIds.length < input.questionIds.length,
      dailyLimit,
      maxFreePerSession,
      quotaScopeType,
      quotaScopeId,
      quota,
      scope: candidates,
      rules: mergedRules,
    },
  };
}

export async function recordPracticeAccessEvent(client: pg.PoolClient, input: {
  tenantId: string;
  userId: string | null;
  practiceSessionId?: string | null;
  eventType: 'session_created' | 'session_denied' | 'quota_consumed';
  accessMode: 'free' | 'svip' | 'staff' | 'denied';
  requestedCount: number;
  grantedCount: number;
  consumedFreeQuota: number;
  reason?: string | null;
  scopeType?: string | null;
  scopeId?: string | null;
  entitlementId?: string | null;
  metadata?: JsonObject;
}) {
  await client.query(
    `
    insert into public.practice_access_events (
      tenant_id, user_id, practice_session_id, event_type, access_mode,
      requested_count, granted_count, consumed_free_quota, reason,
      scope_type, scope_id, entitlement_id, metadata
    )
    values ($1, $2::uuid, $3::uuid, $4, $5, $6, $7, $8, $9, $10, $11::uuid, $12::uuid, $13::jsonb)
    `,
    [
      input.tenantId,
      input.userId,
      input.practiceSessionId || null,
      input.eventType,
      input.accessMode,
      input.requestedCount,
      input.grantedCount,
      input.consumedFreeQuota,
      input.reason || null,
      input.scopeType || null,
      input.scopeId || null,
      input.entitlementId || null,
      JSON.stringify(input.metadata || {}),
    ],
  );
}

export async function assertAnswerSessionAccess(client: pg.PoolClient, input: {
  tenantId: string;
  userId: string;
  practiceSessionId: string | null;
  questionId: string;
}) {
  if (!input.practiceSessionId) {
    throw new HttpError(400, 'practiceSessionId is required for answer submission', 'PRACTICE_SESSION_REQUIRED');
  }
  const result = await client.query<{ id: string }>(
    `
    select id
    from public.practice_sessions
    where tenant_id = $1
      and user_id = $2
      and id = $3
      and finished_at is null
      and (expires_at is null or expires_at > now())
      and question_ids ? $4
    limit 1
    `,
    [input.tenantId, input.userId, input.practiceSessionId, input.questionId],
  );
  if (!result.rows[0]) {
    throw new HttpError(403, 'Question is not available in the active practice session', 'PRACTICE_SESSION_QUESTION_FORBIDDEN');
  }
}
