import { and, eq, gt, inArray, isNull, notInArray, sql } from 'drizzle-orm';
import type { DrizzleDb, TxDrizzleClient } from '@/server/db/client.js';
import {
  order,
  orderLine,
  payouts,
  sessions,
  users,
  vendorPayoutReleases,
  vendors,
} from '@/server/db/schema.js';
import { recordAdminAction } from '@/server/db/queries/admin-actions.js';
import type { SettlementAdminPrincipal } from '@/server/admin/resources/settlements/commands.js';
import { vendorSplit } from '@platform-modules/commerce-orders';

const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
// JWT-authenticated requests carry a synthetic session id (`jwt:<sub>:<iat>`)
// with no sessions-table row; <sub> must match the acting user id.
const JWT_SESSION_PATTERN =
  /^jwt:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}:\d+$/i;
const PERIOD_MONTH_PATTERN = /^\d{4}-(0[1-9]|1[0-2])$/;

function isJwtSessionId(sessionId: string): boolean {
  return JWT_SESSION_PATTERN.test(sessionId);
}

function isValidSessionPrincipal(session: { id: string; userId: string }): boolean {
  if (UUID_PATTERN.test(session.id)) return true;
  return isJwtSessionId(session.id) && session.id.split(':')[1] === session.userId.toLowerCase();
}

export class AdminOwnVendorSettlementError extends Error {
  constructor() {
    super('Admin cannot modify settlements for their own vendor');
    this.name = 'AdminOwnVendorSettlementError';
  }
}

export class SettlementAdminAuthorizationError extends Error {
  constructor() {
    super('Admin authorization required');
    this.name = 'SettlementAdminAuthorizationError';
  }
}

export class SettlementStateConflictError extends Error {
  constructor(message = 'Settlement state conflicts with requested transition') {
    super(message);
    this.name = 'SettlementStateConflictError';
  }
}

export class SettlementPeriodNotFoundError extends Error {
  constructor() {
    super('No earnings found for vendor/period');
    this.name = 'SettlementPeriodNotFoundError';
  }
}

export class SettlementWriteValidationError extends Error {
  constructor() {
    super('Invalid settlement write');
    this.name = 'SettlementWriteValidationError';
  }
}

export interface AdminSettlementWriteInput {
  vendorId: string;
  periodMonth: string;
  actor: SettlementAdminPrincipal;
  notes?: string;
}

function assertValidWriteInput(input: AdminSettlementWriteInput): void {
  if (
    !input ||
    typeof input !== 'object' ||
    !input.actor ||
    typeof input.actor !== 'object' ||
    !input.actor.session ||
    typeof input.actor.session !== 'object' ||
    !input.actor.user ||
    typeof input.actor.user !== 'object'
  ) {
    throw new SettlementWriteValidationError();
  }
  if (
    !UUID_PATTERN.test(input.vendorId) ||
    !PERIOD_MONTH_PATTERN.test(input.periodMonth) ||
    !UUID_PATTERN.test(input.actor.session.userId) ||
    !isValidSessionPrincipal(input.actor.session) ||
    !UUID_PATTERN.test(input.actor.user.id) ||
    (input.notes !== undefined && (typeof input.notes !== 'string' || input.notes.length > 500))
  ) {
    throw new SettlementWriteValidationError();
  }
}

async function assertActiveAdmin(
  tx: TxDrizzleClient,
  actor: SettlementAdminPrincipal,
): Promise<void> {
  if (!actor.user.isAdmin || actor.session.userId !== actor.user.id) {
    throw new SettlementAdminAuthorizationError();
  }
  if (isJwtSessionId(actor.session.id)) {
    // JWT sessions have no sessions-table row: the token was verified
    // cryptographically upstream. Re-verify the user row inside the txn.
    const [admin] = await tx
      .select({ isAdmin: users.isAdmin, accountState: users.accountState })
      .from(users)
      .where(eq(users.id, actor.user.id))
      .for('share');
    if (!admin?.isAdmin || admin.accountState !== 'ACTIVE') {
      throw new SettlementAdminAuthorizationError();
    }
    return;
  }
  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 SettlementAdminAuthorizationError();
  }
}

async function assertAdminDoesNotOwnVendor(
  tx: TxDrizzleClient,
  vendorId: string,
  adminUserId: string,
): Promise<void> {
  const [vendor] = await tx
    .select({ ownerUserId: vendors.ownerUserId })
    .from(vendors)
    .where(eq(vendors.id, vendorId))
    .for('update');
  if (!vendor) throw new Error(`Vendor not found: ${vendorId}`);
  if (vendor.ownerUserId === adminUserId) throw new AdminOwnVendorSettlementError();
}

export async function upsertPaidPayout(
  db: DrizzleDb,
  input: AdminSettlementWriteInput,
  conflictError: Error,
) {
  assertValidWriteInput(input);
  const now = new Date();
  return db.transaction(
    async (tx: TxDrizzleClient) => {
      await tx.execute(
        sql`SELECT pg_advisory_xact_lock(hashtext(${input.vendorId} || ':' || ${input.periodMonth}))`,
      );
      await assertActiveAdmin(tx, input.actor);
      await assertAdminDoesNotOwnVendor(tx, input.vendorId, input.actor.user.id);
      const [existing] = await tx
        .select()
        .from(payouts)
        .where(
          and(eq(payouts.vendorId, input.vendorId), eq(payouts.periodMonth, input.periodMonth)),
        )
        .for('update');
      if (existing && existing.status !== 'pending' && existing.status !== 'paid') {
        throw new SettlementStateConflictError();
      }
      const [blockedRelease] = await tx
        .select({ id: vendorPayoutReleases.id })
        .from(vendorPayoutReleases)
        .innerJoin(orderLine, eq(vendorPayoutReleases.orderLineId, orderLine.id))
        .innerJoin(order, eq(orderLine.orderId, order.id))
        .where(
          and(
            eq(vendorPayoutReleases.vendorId, input.vendorId),
            sql`to_char(date_trunc('month', ${order.createdAt} AT TIME ZONE 'UTC'), 'YYYY-MM') = ${input.periodMonth}`,
            notInArray(vendorPayoutReleases.status, ['released', 'refunded', 'cancelled']),
          ),
        )
        .limit(1);
      if (blockedRelease) throw conflictError;
      const [earnings] = await tx
        .select({
          orderCount: sql<number>`COUNT(DISTINCT ${order.id})::int`,
          amountAgorot: sql<number>`COALESCE(SUM(CASE WHEN ${vendorSplit.funder} = 'vendor' THEN ${vendorSplit.amount} ELSE 0 END), 0)::int`,
          platformFeeAgorot: sql<number>`COALESCE(SUM(CASE WHEN ${vendorSplit.funder} = 'platform' THEN ${vendorSplit.amount} ELSE 0 END), 0)::int`,
        })
        .from(vendorSplit)
        .innerJoin(order, eq(vendorSplit.orderId, order.id))
        .where(
          and(
            eq(vendorSplit.vendorId, input.vendorId),
            sql`to_char(date_trunc('month', ${order.createdAt} AT TIME ZONE 'UTC'), 'YYYY-MM') = ${input.periodMonth}`,
            inArray(order.status, ['paid', 'fulfilled', 'completed']),
          ),
        )
        .limit(1);
      if (!earnings || earnings.orderCount === 0) throw new SettlementPeriodNotFoundError();
      if (existing?.status === 'paid') {
        if (
          existing.amountAgorot !== earnings.amountAgorot ||
          existing.platformFeeAgorot !== earnings.platformFeeAgorot
        ) {
          throw new SettlementStateConflictError('Paid settlement amounts are immutable');
        }
        return existing;
      }
      const values = {
        amountAgorot: earnings.amountAgorot,
        platformFeeAgorot: earnings.platformFeeAgorot,
        status: 'paid',
        markedPaidAt: now,
        markedPaidBy: input.actor.user.id,
        notes: input.notes ?? null,
        updatedAt: now,
      } as const;
      const [payout] = existing
        ? await tx.update(payouts).set(values).where(eq(payouts.id, existing.id)).returning()
        : await tx
            .insert(payouts)
            .values({
              vendorId: input.vendorId,
              periodMonth: input.periodMonth,
              ...values,
            })
            .returning();
      if (!payout)
        throw new Error(
          `markPaid: upsert returned no row for ${input.vendorId}/${input.periodMonth}`,
        );
      await recordAdminAction(tx, {
        adminId: input.actor.user.id,
        targetType: 'SETTLEMENT',
        targetId: payout.id,
        action: 'MARK_PAID',
        note: input.notes ?? null,
      });
      return payout;
    },
    { isolationLevel: 'serializable' },
  );
}
