import { and, eq, gt, isNull } from 'drizzle-orm';
import { z } from 'zod';
import { affiliateEnrollments, sessions, users } from '@/server/db/schema';
import type { DrizzleDb, TxDrizzleClient } from '@/server/db/client';
import type { Session, User } from '@/server/auth/session.js';
import { ForbiddenError, NotFoundError, ValidationError } from '@/server/api/domain-error.js';
import {
  recordAffiliateAdminAction,
  updateAffiliateEnrollment,
} from '@/server/db/queries/referrals/referral-writes';

const affiliateActionSchema = z.discriminatedUnion('action', [
  z.strictObject({ action: z.literal('suspend'), reason: z.string().max(500).optional() }),
  z.strictObject({ action: z.literal('revoke'), reason: z.string().max(500).optional() }),
  z.strictObject({ action: z.literal('reinstate') }),
  z.strictObject({ action: z.literal('set_pct'), pct: z.number().int().min(0).max(100) }),
]);

export type AffiliateAction = z.infer<typeof affiliateActionSchema>;

export type ValidationResult = { ok: true } | { ok: false; reason: string };
export interface AffiliateAdminPrincipal {
  session: Pick<Session, 'id' | 'userId'>;
  user: Pick<User, 'id' | 'isAdmin'>;
}

const idSchema = z.uuid();
const fraudResolutionActionSchema = z.enum(['release', 'reject_clawback', 'suspend', 'dismiss']);

export class AffiliateAdminAuthorizationError extends ForbiddenError {
  constructor() {
    super('Admin authorization required', 'AFFILIATE_ADMIN_REQUIRED');
    this.name = 'AffiliateAdminAuthorizationError';
  }
}

export class AffiliateSelfActionError extends ForbiddenError {
  constructor() {
    super('Admin cannot perform a self-beneficial affiliate action', 'AFFILIATE_SELF_ACTION');
    this.name = 'AffiliateSelfActionError';
  }
}

export class AffiliateActionValidationError extends ValidationError {
  constructor(reason: string) {
    super(reason, 'AFFILIATE_ACTION_INVALID');
    this.name = 'AffiliateActionValidationError';
  }
}

export class AffiliateEnrollmentNotFoundError extends NotFoundError {
  constructor() {
    super('Enrollment not found', 'AFFILIATE_ENROLLMENT_NOT_FOUND');
    this.name = 'AffiliateEnrollmentNotFoundError';
  }
}

function validateAction(status: string, actionData: AffiliateAction): ValidationResult {
  if (status !== 'active' && status !== 'suspended' && status !== 'revoked') {
    return { ok: false, reason: `Unknown enrollment status "${status}"` };
  }
  switch (actionData.action) {
    case 'suspend':
      return status === 'active'
        ? { ok: true }
        : { ok: false, reason: `Cannot suspend enrollment with status "${status}"` };
    case 'revoke':
      return status === 'revoked'
        ? { ok: false, reason: 'Enrollment is already revoked' }
        : { ok: true };
    case 'reinstate':
      return status === 'suspended'
        ? { ok: true }
        : { ok: false, reason: `Cannot reinstate enrollment with status "${status}"` };
    case 'set_pct':
      if (!Number.isFinite(actionData.pct) || !Number.isInteger(actionData.pct)) {
        return { ok: false, reason: 'Commission percentage must be a finite integer' };
      }
      if (actionData.pct < 0 || actionData.pct > 100) {
        return { ok: false, reason: 'Commission percentage must be between 0 and 100' };
      }
      return status === 'revoked'
        ? { ok: false, reason: 'Cannot update commission for a revoked enrollment' }
        : { ok: true };
  }
}

function validateAdminPrincipal(actor: AffiliateAdminPrincipal): void {
  if (!actor.user.isAdmin || actor.session.userId !== actor.user.id) {
    throw new AffiliateAdminAuthorizationError();
  }
  if (!idSchema.safeParse(actor.session.id).success || !idSchema.safeParse(actor.user.id).success) {
    throw new AffiliateActionValidationError('Session and admin IDs must be UUIDs');
  }
}

async function assertPersistedActiveAdmin(
  tx: TxDrizzleClient,
  actor: AffiliateAdminPrincipal,
): Promise<void> {
  const [admin] = await tx
    .select({
      userId: sessions.userId,
      isAdmin: users.isAdmin,
      accountState: users.accountState,
    })
    .from(sessions)
    .innerJoin(users, eq(users.id, sessions.userId))
    .where(
      and(
        eq(sessions.id, actor.session.id),
        eq(sessions.userId, actor.user.id),
        isNull(sessions.revokedAt),
        gt(sessions.expiresAt, new Date()),
      ),
    )
    .for('share');
  if (!admin?.isAdmin || admin.accountState !== 'ACTIVE') {
    throw new AffiliateAdminAuthorizationError();
  }
}

export async function assertAffiliateFraudResolutionAuthorizedInTx(
  tx: TxDrizzleClient,
  actor: AffiliateAdminPrincipal,
  targetUserId: string,
  action: string,
): Promise<void> {
  validateAdminPrincipal(actor);
  if (!idSchema.safeParse(targetUserId).success) {
    throw new AffiliateActionValidationError('Target user ID must be a UUID');
  }
  const parsedAction = fraudResolutionActionSchema.safeParse(action);
  if (!parsedAction.success) throw new AffiliateActionValidationError('Invalid fraud action');

  await assertPersistedActiveAdmin(tx, actor);
  if (targetUserId === actor.user.id) {
    throw new AffiliateSelfActionError();
  }
}

export async function executeAffiliateActionInTx(
  tx: TxDrizzleClient,
  enrollmentId: string,
  actionData: AffiliateAction,
  actor: AffiliateAdminPrincipal,
): Promise<void> {
  validateAdminPrincipal(actor);
  if (!idSchema.safeParse(enrollmentId).success) {
    throw new AffiliateActionValidationError('Enrollment ID must be a UUID');
  }
  const parsedAction = affiliateActionSchema.safeParse(actionData);
  if (!parsedAction.success) throw new AffiliateActionValidationError('Invalid affiliate action');
  actionData = parsedAction.data;

  await assertPersistedActiveAdmin(tx, actor);

  const [before] = await tx
    .select({
      userId: affiliateEnrollments.userId,
      status: affiliateEnrollments.status,
      commissionPct: affiliateEnrollments.commissionPct,
    })
    .from(affiliateEnrollments)
    .where(eq(affiliateEnrollments.id, enrollmentId))
    .for('update');
  if (!before) throw new AffiliateEnrollmentNotFoundError();

  const validation = validateAction(before.status, actionData);
  if (!validation.ok) throw new AffiliateActionValidationError(validation.reason);
  if (
    before.userId === actor.user.id &&
    (actionData.action === 'reinstate' || actionData.action === 'set_pct')
  ) {
    throw new AffiliateSelfActionError();
  }

  let afterStatus: string | undefined;
  let afterPct: number | undefined;
  let values: Partial<typeof affiliateEnrollments.$inferInsert>;
  switch (actionData.action) {
    case 'suspend':
      values = {
        status: 'suspended',
        suspendedAt: new Date(),
        suspendedReason: actionData.reason ?? null,
      };
      afterStatus = 'suspended';
      break;
    case 'revoke':
      values = {
        status: 'revoked',
        suspendedAt: new Date(),
        suspendedReason: actionData.reason ?? 'Revoked by admin',
      };
      afterStatus = 'revoked';
      break;
    case 'reinstate':
      values = { status: 'active', suspendedAt: null, suspendedReason: null };
      afterStatus = 'active';
      break;
    case 'set_pct':
      values = { commissionPct: actionData.pct };
      afterPct = actionData.pct;
      break;
  }

  const updated = await updateAffiliateEnrollment(tx, enrollmentId, values);
  if (updated.length !== 1) throw new AffiliateEnrollmentNotFoundError();

  await recordAffiliateAdminAction(tx, {
    adminUserId: actor.user.id,
    targetUserId: before.userId,
    action: actionData.action,
    payload: {
      enrollmentId,
      before: { status: before.status, commissionPct: before.commissionPct },
      after: {
        status: afterStatus ?? before.status,
        commissionPct: afterPct ?? before.commissionPct,
      },
      ...(actionData.action === 'set_pct' ? { pct: actionData.pct } : {}),
    },
    reason: 'reason' in actionData ? (actionData.reason ?? null) : null,
  });
}

export async function executeAffiliateAction(
  db: DrizzleDb,
  enrollmentId: string,
  actionData: AffiliateAction,
  actor: AffiliateAdminPrincipal,
): Promise<void> {
  await db.transaction((tx) => executeAffiliateActionInTx(tx, enrollmentId, actionData, actor));
}
