/**
 * Promo-code queries — typed async helpers for the promo_codes table.
 */

import { desc, eq, inArray, sql } from 'drizzle-orm';
import type { DrizzleClient, TxDrizzleClient } from '../client.js';
import { order, orderLine, promoAllowlist, promoCodes } from '../schema.js';
import type { PromoCode } from '@/server/promo/types.js';

type HostRulesJson = {
  scope?: PromoRulesScope;
  eligibility?: PromoRulesEligibility;
};

type PromoRulesScope =
  | { kind: 'all' }
  | { kind: 'deals'; dealIds: string[] }
  | { kind: 'categories'; categoryIds: string[] }
  | { kind: 'tags'; tagIds: string[] }
  | { kind: 'vendor'; vendorId: string };

type PromoRulesEligibility = {
  firstPurchaseOnly?: boolean;
  clubOnly?: boolean;
  userAllowlist?: string[];
};

type PromoRow = typeof promoCodes.$inferSelect;
type PromoInsert = typeof promoCodes.$inferInsert;

/** Host-facing create input — scope/eligibility are derived from rulesJson by toPromoWriteValues. */
export type CreatePromoCodeInput = Omit<PromoInsert, 'scope' | 'eligibility'> & {
  scope?: PromoInsert['scope'];
  eligibility?: PromoInsert['eligibility'];
};

function normalizeRulesJson(rulesJson: unknown): HostRulesJson {
  if (!rulesJson || typeof rulesJson !== 'object') {
    return { scope: { kind: 'all' }, eligibility: {} };
  }
  const parsed = rulesJson as HostRulesJson;
  return {
    scope: parsed.scope ?? { kind: 'all' },
    eligibility: parsed.eligibility ?? {},
  };
}

function getAllowlistFromRules(rulesJson: unknown): string[] {
  return normalizeRulesJson(rulesJson).eligibility?.userAllowlist ?? [];
}

function withAllowlist(rulesJson: unknown, userAllowlist: string[]): HostRulesJson {
  const rules = normalizeRulesJson(rulesJson);
  const nextEligibility: PromoRulesEligibility = {
    ...(rules.eligibility ?? {}),
  };
  if (userAllowlist.length > 0) {
    nextEligibility.userAllowlist = userAllowlist;
  } else {
    delete nextEligibility.userAllowlist;
  }
  return { ...rules, eligibility: nextEligibility };
}

function toPlatformScope(scope: PromoRulesScope | undefined): PromoInsert['scope'] {
  if (!scope || scope.kind === 'all') return { kind: 'all' };
  if (scope.kind === 'deals') return { kind: 'products', ids: scope.dealIds };
  if (scope.kind === 'categories') return { kind: 'categories', ids: scope.categoryIds };
  if (scope.kind === 'tags') return { kind: 'tags', tags: scope.tagIds };
  return { kind: 'vendor', vendorId: scope.vendorId };
}

function toPlatformEligibility(
  eligibility: PromoRulesEligibility | undefined,
): PromoInsert['eligibility'] {
  return {
    ...(eligibility?.firstPurchaseOnly ? { firstPurchaseOnly: true } : {}),
    ...(eligibility?.clubOnly ? { membersOnly: true } : {}),
    ...(eligibility?.userAllowlist?.length ? { allowlistOnly: true } : {}),
  };
}

function toPromoWriteValues(values: Partial<PromoInsert>): Partial<PromoInsert> {
  const rules = values.rulesJson === undefined ? undefined : normalizeRulesJson(values.rulesJson);
  const totalCap = values.totalCap ?? undefined;
  const minSubtotal = values.minSubtotal ?? undefined;
  const maxSubtotal = values.maxSubtotal ?? undefined;
  const valueAmount = values.valueAmount ?? undefined;

  return {
    ...values,
    ...(rules
      ? {
          rulesJson: withAllowlist(rules, []),
          scope: toPlatformScope(rules.scope),
          eligibility: toPlatformEligibility(rules.eligibility),
        }
      : {}),
    ...(values.status !== undefined ? { active: values.status === 'active' } : {}),
    ...(values.validFrom !== undefined ? { startsAt: values.validFrom } : {}),
    ...(values.validUntil !== undefined ? { endsAt: values.validUntil } : {}),
    ...(totalCap !== undefined ? { maxUses: totalCap } : {}),
    ...(minSubtotal !== undefined ? { minOrderAmount: BigInt(minSubtotal) } : {}),
    ...(maxSubtotal !== undefined ? { maxOrderAmount: BigInt(maxSubtotal) } : {}),
    ...(valueAmount !== undefined || minSubtotal !== undefined || maxSubtotal !== undefined
      ? { currency: 'ILS' }
      : {}),
  };
}

function hydratePromoRow(row: PromoRow, allowlist: string[]): PromoCode {
  return {
    ...row,
    totalCap: row.totalCap ?? row.maxUses ?? null,
    redemptionCount: row.uses ?? row.redemptionCount,
    rulesJson: withAllowlist(row.rulesJson, allowlist),
  };
}

async function loadAllowlistMap(
  db: DrizzleClient,
  promoIds: string[],
): Promise<Map<string, string[]>> {
  const out = new Map<string, string[]>();
  if (promoIds.length === 0) return out;

  const rows = await db
    .select({ promoId: promoAllowlist.promoId, userId: promoAllowlist.userId })
    .from(promoAllowlist)
    .where(inArray(promoAllowlist.promoId, promoIds));

  for (const row of rows) {
    const list = out.get(row.promoId) ?? [];
    list.push(row.userId);
    out.set(row.promoId, list);
  }

  return out;
}

async function syncAllowlist(
  db: DrizzleClient,
  promoId: string,
  userAllowlist: string[],
): Promise<void> {
  await db.delete(promoAllowlist).where(eq(promoAllowlist.promoId, promoId));
  if (userAllowlist.length === 0) return;

  await db
    .insert(promoAllowlist)
    .values(userAllowlist.map((userId) => ({ promoId, userId })))
    .onConflictDoNothing();
}

export async function getPromoCodeByCode(
  db: DrizzleClient,
  code: string,
): Promise<PromoCode | null> {
  const rows = await db
    .select()
    .from(promoCodes)
    .where(sql`upper(${promoCodes.code}) = upper(${code})`)
    .limit(1);
  const row = rows[0];
  if (!row) return null;
  const allowlistMap = await loadAllowlistMap(db, [row.id]);
  return hydratePromoRow(row, allowlistMap.get(row.id) ?? []);
}

export async function getPromoCodeById(db: DrizzleClient, id: string): Promise<PromoCode | null> {
  const rows = await db.select().from(promoCodes).where(eq(promoCodes.id, id)).limit(1);
  const row = rows[0];
  if (!row) return null;
  const allowlistMap = await loadAllowlistMap(db, [row.id]);
  return hydratePromoRow(row, allowlistMap.get(row.id) ?? []);
}

export async function createPromoCode(
  db: DrizzleClient,
  values: CreatePromoCodeInput,
): Promise<PromoCode> {
  return (db as TxDrizzleClient).transaction(async (tx) => {
    const allowlist = getAllowlistFromRules(values.rulesJson);
    const [row] = await tx
      .insert(promoCodes)
      .values(toPromoWriteValues(values) as PromoInsert)
      .returning();
    await syncAllowlist(tx, row!.id, allowlist);
    return hydratePromoRow(row!, allowlist);
  });
}

export async function updatePromoCode(
  db: DrizzleClient,
  id: string,
  values: Partial<typeof promoCodes.$inferInsert>,
): Promise<PromoCode | null> {
  return (db as TxDrizzleClient).transaction(async (tx) => {
    const existingRows = await tx.select().from(promoCodes).where(eq(promoCodes.id, id)).limit(1);
    const existing = existingRows[0];
    if (!existing) return null;

    const allowlistMap = await loadAllowlistMap(tx, [id]);
    const currentAllowlist = allowlistMap.get(id) ?? [];
    const nextAllowlist =
      values.rulesJson === undefined ? currentAllowlist : getAllowlistFromRules(values.rulesJson);

    const [row] = await tx
      .update(promoCodes)
      .set({ ...toPromoWriteValues(values), updatedAt: new Date() })
      .where(eq(promoCodes.id, id))
      .returning();
    await syncAllowlist(tx, id, nextAllowlist);
    return row ? hydratePromoRow(row, nextAllowlist) : null;
  });
}

export async function listPromoCodesForVendor(
  db: DrizzleClient,
  vendorId: string,
  { page = 1, pageSize = 500 }: { page?: number; pageSize?: number } = {},
): Promise<PromoCode[]> {
  const limit = Math.min(pageSize, 500);
  const offset = (page - 1) * limit;
  const rows = await db
    .select()
    .from(promoCodes)
    .where(eq(promoCodes.vendorId, vendorId))
    .orderBy(desc(promoCodes.createdAt))
    .limit(limit)
    .offset(offset);
  const allowlistMap = await loadAllowlistMap(
    db,
    rows.map((row) => row.id),
  );
  return rows.map((row) => hydratePromoRow(row, allowlistMap.get(row.id) ?? []));
}

export async function listAllPromoCodes(
  db: DrizzleClient,
  { limit = 20, offset = 0 }: { limit?: number; offset?: number } = {},
): Promise<PromoCode[]> {
  const rows = await db
    .select()
    .from(promoCodes)
    .orderBy(desc(promoCodes.createdAt))
    .limit(Math.min(limit, 500))
    .offset(Math.max(0, offset));
  const allowlistMap = await loadAllowlistMap(
    db,
    rows.map((row) => row.id),
  );
  return rows.map((row) => hydratePromoRow(row, allowlistMap.get(row.id) ?? []));
}

export async function countPromoCodes(db: DrizzleClient): Promise<number> {
  const rows = await db.select({ c: sql<number>`count(*)::int` }).from(promoCodes);
  return Number(rows[0]?.c ?? 0);
}

/** Row lock for quota reservation inside checkout transactions. */
export async function lockPromoCodeForUpdate(
  db: DrizzleClient,
  id: string,
): Promise<PromoCode | null> {
  const rows = await db
    .select()
    .from(promoCodes)
    .where(eq(promoCodes.id, id))
    .for('update')
    .limit(1);
  const row = rows[0];
  if (!row) return null;
  const allowlistMap = await loadAllowlistMap(db, [row.id]);
  return hydratePromoRow(row, allowlistMap.get(row.id) ?? []);
}

/** Cap guard only. Runtime usage writer is platform recordRedemption. */
export async function incrementRedemptionCount(
  db: DrizzleClient,
  id: string,
): Promise<number | null> {
  const rows = await db
    .select({
      uses: promoCodes.uses,
      totalCap: promoCodes.totalCap,
      maxUses: promoCodes.maxUses,
    })
    .from(promoCodes)
    .where(eq(promoCodes.id, id))
    .limit(1);
  const row = rows[0];
  if (!row) return null;
  const cap = row.totalCap ?? row.maxUses;
  if (cap != null && row.uses >= cap) return null;
  return row.uses + 1;
}

/** No-op: runtime usage writer is platform recordRedemption. */
export async function decrementRedemptionCount(_db: DrizzleClient, _id: string): Promise<void> {}

/** Authoritative purchase anchor for promo finalization — binds identity + economics. */
export async function getPurchasePromoAnchor(
  db: DrizzleClient | TxDrizzleClient,
  purchaseId: string,
): Promise<{
  userId: string;
  vendorId: string;
  lineTotalAgorot: number;
} | null> {
  const [row] = await db
    .select({
      userId: order.buyerUserId,
      vendorId: orderLine.vendorId,
      lineTotal: orderLine.lineTotal,
    })
    .from(orderLine)
    .innerJoin(order, eq(order.id, orderLine.orderId))
    .where(eq(orderLine.id, purchaseId))
    .limit(1);
  if (!row?.userId || !row.vendorId) return null;
  return {
    userId: row.userId,
    vendorId: row.vendorId,
    lineTotalAgorot: Number(row.lineTotal),
  };
}
