import crypto from "node:crypto";
import type { NextRequest } from "next/server";
import {
  AffiliateAccountStatus,
  AffiliateCpaValidationMode,
  AffiliateCommissionStatus,
  AffiliateOfferStatus,
  AffiliateOfferType,
  AffiliateReferralStatus,
  Prisma,
  Role
} from "@prisma/client";
import { prisma } from "@/server/db";
import { AppError, ForbiddenError } from "@/server/errors";
import { requireUser } from "@/server/security/auth";
import { hasRole } from "@/server/security/rbac";

export const AFFILIATION_COOKIE = "zira_aff";

type Db = typeof prisma | Prisma.TransactionClient;

export type AffiliateDashboardRange = {
  start: Date;
  end: Date;
  label: string;
};

export function affiliatePublicId() {
  return `AFF-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;
}

export function affiliateLinkCode() {
  return crypto.randomBytes(5).toString("base64url").toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 9);
}

export function hashAttributionValue(value?: string | null) {
  if (!value) return undefined;
  return crypto.createHash("sha256").update(value).digest("hex");
}

export function normalizeLandingPath(path?: string | null) {
  if (!path) return "/";
  if (!path.startsWith("/") || path.startsWith("//")) return "/";
  return path.slice(0, 180);
}

export async function requireAffiliate(request: NextRequest) {
  const auth = await requireUser(request);
  const account = await prisma.affiliateAccount.findUnique({
    where: { userId: auth.user.id },
    include: { defaultOffer: true }
  });

  if (!account) {
    throw new ForbiddenError("Affiliate account required");
  }

  if (account.status === AffiliateAccountStatus.CLOSED || account.status === AffiliateAccountStatus.SUSPENDED) {
    throw new ForbiddenError("Affiliate account is not active");
  }

  return { ...auth, account };
}

export async function requireAffiliateAdmin(request: NextRequest) {
  const auth = await requireUser(request);
  if (!hasRole(auth.roles, [Role.ADMIN, Role.SUPER_ADMIN])) {
    throw new ForbiddenError();
  }
  return auth;
}

export function affiliateDashboardRange(searchParams: URLSearchParams): AffiliateDashboardRange {
  const now = new Date();
  const preset = searchParams.get("preset") ?? "30d";
  const explicitFrom = searchParams.get("from");
  const explicitTo = searchParams.get("to");

  if (explicitFrom || explicitTo) {
    const start = explicitFrom ? startOfDay(new Date(explicitFrom)) : startOfDay(daysAgo(now, 29));
    const end = explicitTo ? endOfDay(new Date(explicitTo)) : now;
    return normalizeRange(start, end, "Personnalisé");
  }

  if (preset === "today") return normalizeRange(startOfDay(now), now, "Aujourd'hui");
  if (preset === "yesterday") {
    const yesterday = daysAgo(now, 1);
    return normalizeRange(startOfDay(yesterday), endOfDay(yesterday), "Hier");
  }
  if (preset === "7d") return normalizeRange(startOfDay(daysAgo(now, 6)), now, "7 jours");
  if (preset === "month") return normalizeRange(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)), now, "Mois en cours");
  return normalizeRange(startOfDay(daysAgo(now, 29)), now, "30 jours");
}

export async function getAffiliateDashboard(userId: string, range: AffiliateDashboardRange = affiliateDashboardRange(new URLSearchParams())) {
  const account = await prisma.affiliateAccount.findUnique({
    where: { userId },
    include: {
      defaultOffer: true,
      links: { orderBy: { createdAt: "desc" }, take: 50, include: { offer: true } },
      payouts: { orderBy: { createdAt: "desc" }, take: 6 }
    }
  });

  if (!account) {
    throw new AppError("Affiliate account not found", 404, "AFFILIATE_ACCOUNT_NOT_FOUND");
  }

  const ranged = { gte: range.start, lte: range.end };

  const [
    clicks,
    registrations,
    rejectedGeo,
    ftd,
    qualifiedCpa,
    depositSum,
    commissionSum,
    payableSum,
    recentCommissions,
    rangedClicks,
    rangedReferrals,
    countries,
    recentReferrals,
    linkClickStats,
    linkReferralStats
  ] = await Promise.all([
    prisma.affiliateClick.count({ where: { accountId: account.id, createdAt: ranged } }),
    prisma.affiliateReferral.count({ where: { accountId: account.id, rewardEligible: true, registeredAt: ranged } }),
    prisma.affiliateReferral.count({ where: { accountId: account.id, rewardEligible: false, registeredAt: ranged } }),
    prisma.affiliateReferral.count({ where: { accountId: account.id, rewardEligible: true, ftdAt: ranged } }),
    prisma.affiliateReferral.count({ where: { accountId: account.id, rewardEligible: true, cpaQualifiedAt: ranged } }),
    prisma.affiliateReferral.aggregate({
      where: { accountId: account.id, rewardEligible: true, ftdAt: ranged },
      _sum: { depositTotal: true }
    }),
    prisma.affiliateCommission.aggregate({
      where: { accountId: account.id, createdAt: ranged, status: { not: AffiliateCommissionStatus.REJECTED } },
      _sum: { amount: true }
    }),
    prisma.affiliateCommission.aggregate({
      where: { accountId: account.id, createdAt: ranged, status: { in: [AffiliateCommissionStatus.PAYABLE, AffiliateCommissionStatus.APPROVED] } },
      _sum: { amount: true }
    }),
    prisma.affiliateCommission.findMany({
      where: { accountId: account.id, createdAt: ranged, status: { not: AffiliateCommissionStatus.REJECTED } },
      orderBy: { createdAt: "asc" },
      take: 200
    }),
    prisma.affiliateClick.findMany({
      where: { accountId: account.id, createdAt: ranged },
      select: { createdAt: true },
      orderBy: { createdAt: "asc" },
      take: 1000
    }),
    prisma.affiliateReferral.findMany({
      where: { accountId: account.id, registeredAt: ranged },
      select: { registeredAt: true, ftdAt: true, cpaQualifiedAt: true, rewardEligible: true },
      orderBy: { registeredAt: "asc" },
      take: 1000
    }),
    prisma.affiliateReferral.groupBy({
      by: ["country"],
      where: { accountId: account.id, rewardEligible: true, country: { not: null }, registeredAt: ranged },
      _count: { _all: true },
      orderBy: { _count: { country: "desc" } },
      take: 5
    }),
    prisma.affiliateReferral.findMany({
      where: { accountId: account.id, registeredAt: ranged },
      orderBy: { registeredAt: "desc" },
      take: 12,
      include: { link: true, user: { select: { email: true } } }
    }),
    prisma.affiliateClick.groupBy({
      by: ["linkId"],
      where: { accountId: account.id, createdAt: ranged },
      _count: { _all: true }
    }),
    prisma.affiliateReferral.groupBy({
      by: ["linkId"],
      where: { accountId: account.id, registeredAt: ranged },
      _count: { _all: true }
    })
  ]);

  const clickCountByLink = new Map(linkClickStats.map((item) => [item.linkId, item._count._all]));
  const referralCountByLink = new Map(linkReferralStats.map((item) => [item.linkId, item._count._all]));

  return {
    account: serializeAccount(account),
    range: {
      label: range.label,
      start: range.start.toISOString(),
      end: range.end.toISOString()
    },
    offer: account.defaultOffer ? serializeOffer(account.defaultOffer) : null,
    links: account.links.map((link) => ({
      ...serializeLink(link),
      offer: link.offer ? serializeOffer(link.offer) : null,
      stats: {
        clicks: clickCountByLink.get(link.id) ?? 0,
        registrations: referralCountByLink.get(link.id) ?? 0
      }
    })),
    stats: {
      clicks,
      registrations,
      rejectedGeo,
      ftd,
      qualifiedCpa,
      deposits: depositSum._sum.depositTotal?.toString() ?? "0",
      commission: commissionSum._sum.amount?.toString() ?? "0",
      payable: payableSum._sum.amount?.toString() ?? "0"
    },
    chart: buildDailyCommissionSeries(recentCommissions, range.start, range.end),
    activityChart: buildDailyActivitySeries(rangedClicks, rangedReferrals, range.start, range.end),
    countries: countries.map((row) => ({ country: row.country ?? "N/A", count: row._count._all })),
    recentReferrals: recentReferrals.map((referral) => ({
      id: referral.id,
      email: referral.user?.email ?? null,
      country: referral.country,
      registeredAt: referral.registeredAt.toISOString(),
      firstDepositAt: referral.ftdAt?.toISOString() ?? null,
      cpaQualifiedAt: referral.cpaQualifiedAt?.toISOString() ?? null,
      depositTotal: referral.depositTotal.toString(),
      rewardEligible: referral.rewardEligible,
      rejectionReason: referral.rejectionReason,
      link: referral.link ? { id: referral.link.id, label: referral.link.label, code: referral.link.code } : null
    })),
    payouts: account.payouts.map(serializePayout),
    nextPayoutWindow: nextPayoutWindow(new Date())
  };
}

export async function getAffiliateAdminDashboard() {
  const now = new Date();
  const chartStart = new Date(now);
  chartStart.setDate(chartStart.getDate() - 29);
  chartStart.setHours(0, 0, 0, 0);

  const [accounts, clicks, referrals, ftd, commissions, payable, recentCommissions, offers, links, payouts, recentAccounts, topAffiliates] = await Promise.all([
    prisma.affiliateAccount.count(),
    prisma.affiliateClick.count(),
    prisma.affiliateReferral.count(),
    prisma.affiliateReferral.count({ where: { ftdAt: { not: null } } }),
    prisma.affiliateCommission.aggregate({ where: { status: { not: AffiliateCommissionStatus.REJECTED } }, _sum: { amount: true } }),
    prisma.affiliateCommission.aggregate({ where: { status: { in: [AffiliateCommissionStatus.PAYABLE, AffiliateCommissionStatus.APPROVED] } }, _sum: { amount: true } }),
    prisma.affiliateCommission.findMany({
      where: { createdAt: { gte: chartStart }, status: { not: AffiliateCommissionStatus.REJECTED } },
      orderBy: { createdAt: "asc" },
      take: 300
    }),
    prisma.affiliateOffer.findMany({ where: { status: { not: AffiliateOfferStatus.ARCHIVED } }, orderBy: { createdAt: "desc" }, take: 50 }),
    prisma.affiliateLink.findMany({
      orderBy: { createdAt: "desc" },
      take: 100,
      include: { account: { include: { user: true } }, offer: true }
    }),
    prisma.affiliatePayout.findMany({ orderBy: { createdAt: "desc" }, take: 30, include: { account: true } }),
    prisma.affiliateAccount.findMany({ orderBy: { createdAt: "desc" }, take: 50, include: { user: true, defaultOffer: true } }),
    prisma.affiliateCommission.groupBy({
      by: ["accountId"],
      where: { status: { not: AffiliateCommissionStatus.REJECTED } },
      _sum: { amount: true },
      orderBy: { _sum: { amount: "desc" } },
      take: 8
    })
  ]);

  const topAccountIds = topAffiliates.map((item) => item.accountId);
  const topAccounts = topAccountIds.length
    ? await prisma.affiliateAccount.findMany({ where: { id: { in: topAccountIds } }, include: { user: true } })
    : [];

  return {
    stats: {
      affiliates: accounts,
      clicks,
      registrations: referrals,
      ftd,
      commissions: commissions._sum.amount?.toString() ?? "0",
      payable: payable._sum.amount?.toString() ?? "0"
    },
    chart: buildDailyCommissionSeries(recentCommissions, chartStart, now),
    offers: offers.map(serializeOffer),
    links: links.map((link) => ({
      ...serializeLink(link),
      accountId: link.accountId,
      affiliate: link.account.displayName,
      affiliateEmail: link.account.user.email,
      offer: link.offer ? { id: link.offer.id, code: link.offer.code, name: link.offer.name } : null
    })),
    payouts: payouts.map((payout) => ({ ...serializePayout(payout), affiliate: payout.account.displayName })),
    recentAccounts: recentAccounts.map((account) => ({
      ...serializeAccount(account),
      email: account.user.email,
      offer: account.defaultOffer ? serializeOffer(account.defaultOffer) : null
    })),
    topAffiliates: topAffiliates.map((item) => {
      const account = topAccounts.find((candidate) => candidate.id === item.accountId);
      return {
        accountId: item.accountId,
        displayName: account?.displayName ?? "Affilié",
        email: account?.user.email ?? "",
        amount: item._sum.amount?.toString() ?? "0"
      };
    }),
    nextPayoutWindow: nextPayoutWindow(now)
  };
}

export async function createAffiliateLink(input: { userId: string; label: string; landingPath?: string }) {
  throw new ForbiddenError("Les liens d'affiliation sont gérés par l'administration.");
}

export async function trackAffiliateClick(code: string, request: NextRequest) {
  const link = await prisma.affiliateLink.findUnique({
    where: { code },
    include: { account: true }
  });

  if (!link || link.status !== "active" || link.account.status !== AffiliateAccountStatus.ACTIVE) {
    return null;
  }

  await prisma.affiliateClick.create({
    data: {
      accountId: link.accountId,
      linkId: link.id,
      ipHash: hashAttributionValue(request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip")),
      userAgentHash: hashAttributionValue(request.headers.get("user-agent")),
      landingPath: link.landingPath,
      referrer: request.headers.get("referer")?.slice(0, 300)
    }
  });

  return link;
}

export async function attachAffiliateReferralInTx(
  tx: Prisma.TransactionClient,
  input: { userId: string; country?: string | null; code?: string | null }
) {
  if (!input.code) return null;
  const link = await tx.affiliateLink.findUnique({
    where: { code: input.code },
    include: { account: true, offer: true }
  });

  if (!link || link.status !== "active" || link.account.status !== AffiliateAccountStatus.ACTIVE) {
    return null;
  }

  const offerId = link.offerId ?? link.account.defaultOfferId;
  const country = normalizeCountry(input.country);
  const rewardEligible = isCountryAllowed(link.allowedCountries, country);
  return tx.affiliateReferral.upsert({
    where: { userId: input.userId },
    create: {
      accountId: link.accountId,
      linkId: link.id,
      offerId,
      userId: input.userId,
      country,
      rewardEligible,
      rejectionReason: rewardEligible ? undefined : "GEO_NOT_ALLOWED",
      status: rewardEligible ? AffiliateReferralStatus.REGISTERED : AffiliateReferralStatus.REJECTED
    },
    update: {}
  });
}

export async function applyAffiliateFirstDepositCommissionInTx(
  tx: Prisma.TransactionClient,
  input: { userId: string; eurAmount: Prisma.Decimal; referenceId: string }
) {
  const referral = await tx.affiliateReferral.findUnique({
    where: { userId: input.userId },
    include: { offer: true, link: true }
  });

  if (!referral || !referral.rewardEligible || !referral.link || referral.link.status !== "active") {
    return null;
  }

  const now = new Date();
  const isFirstDeposit = referral.depositCount === 0;
  const depositTotal = referral.depositTotal.add(input.eurAmount);
  const cpaThreshold = referral.link.minCpaDepositAmount;
  const qualifiedByDeposit =
    referral.link.cpaValidationMode === AffiliateCpaValidationMode.CUMULATIVE_DEPOSIT
      ? depositTotal.gte(cpaThreshold)
      : isFirstDeposit && input.eurAmount.gte(cpaThreshold);
  const cpaWindowOk = referral.link.cpaWindowDays ? withinCpaWindow(referral.registeredAt, now, referral.link.cpaWindowDays) : true;
  const alreadyQualified = Boolean(referral.cpaQualifiedAt);
  const shouldQualifyCpa = !alreadyQualified && cpaWindowOk && qualifiedByDeposit;

  const updatedReferral = await tx.affiliateReferral.update({
    where: { id: referral.id },
    data: {
      depositCount: { increment: 1 },
      depositTotal,
      firstDepositAt: isFirstDeposit ? now : undefined,
      firstDepositAmount: isFirstDeposit ? input.eurAmount : undefined,
      ftdAt: isFirstDeposit ? now : referral.ftdAt,
      ftdAmount: isFirstDeposit ? input.eurAmount : referral.ftdAmount,
      status: shouldQualifyCpa ? AffiliateReferralStatus.QUALIFIED : referral.status,
      qualifiedAt: shouldQualifyCpa ? now : referral.qualifiedAt,
      cpaQualifiedAt: shouldQualifyCpa ? now : referral.cpaQualifiedAt
    }
  });

  if (!shouldQualifyCpa) return updatedReferral;

  const commissionAmount = cpaCommissionAmount(referral.link);
  if (!commissionAmount || commissionAmount.lte(0)) return updatedReferral;

  const period = monthPeriod(now);
  const payout = payoutWindowForPeriodEnd(period.end);
  await tx.affiliateCommission.create({
    data: {
      accountId: referral.accountId,
      referralId: referral.id,
      offerId: referral.offerId,
      type: referral.link.rewardType,
      amount: commissionAmount,
      currency: referral.offer?.currency ?? "EUR",
      status: AffiliateCommissionStatus.PENDING,
      periodStart: period.start,
      periodEnd: period.end,
      payableFrom: payout.start,
      description: "Commission CPA qualifiée",
      metadata: {
        referenceId: input.referenceId,
        depositAmount: input.eurAmount.toString(),
        depositTotal: depositTotal.toString(),
        linkId: referral.linkId,
        cpaValidationMode: referral.link.cpaValidationMode
      }
    }
  });

  return updatedReferral;
}

export async function applyAffiliateRevenueShareCommissionInTx(
  tx: Prisma.TransactionClient,
  input: { userId: string; netRevenueEur: Prisma.Decimal; referenceId: string; description?: string }
) {
  const referral = await tx.affiliateReferral.findUnique({
    where: { userId: input.userId },
    include: { offer: true, link: true }
  });

  if (!referral || !referral.rewardEligible || !referral.link || referral.link.status !== "active") {
    return null;
  }

  if (referral.link.rewardType !== AffiliateOfferType.RS && referral.link.rewardType !== AffiliateOfferType.HYBRID) {
    return null;
  }

  const revenueShareBps = referral.link.revenueShareBps ?? 0;
  if (revenueShareBps <= 0) return null;

  const netRevenue = input.netRevenueEur;
  const updatedReferral = await tx.affiliateReferral.update({
    where: { id: referral.id },
    data: { netRevenue: { increment: netRevenue } }
  });

  if (netRevenue.lte(0)) return updatedReferral;

  const commissionAmount = netRevenue.mul(revenueShareBps).div(10000).toDecimalPlaces(6);
  if (commissionAmount.lte(0)) return updatedReferral;

  const now = new Date();
  const period = monthPeriod(now);
  const payout = payoutWindowForPeriodEnd(period.end);
  await tx.affiliateCommission.create({
    data: {
      accountId: referral.accountId,
      referralId: referral.id,
      offerId: referral.offerId,
      type: AffiliateOfferType.RS,
      amount: commissionAmount,
      currency: referral.offer?.currency ?? "EUR",
      status: AffiliateCommissionStatus.PENDING,
      periodStart: period.start,
      periodEnd: period.end,
      payableFrom: payout.start,
      description: input.description ?? "Revenue Share sportsbook",
      metadata: {
        referenceId: input.referenceId,
        linkId: referral.linkId,
        netRevenue: netRevenue.toString(),
        revenueShareBps
      }
    }
  });

  return updatedReferral;
}

function cpaCommissionAmount(link: { rewardType: AffiliateOfferType; cpaAmount: Prisma.Decimal | null }) {
  if (link.rewardType === AffiliateOfferType.CPA || link.rewardType === AffiliateOfferType.HYBRID) return link.cpaAmount;
  return null;
}

export function monthPeriod(date: Date) {
  const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
  const end = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0, 23, 59, 59, 999));
  return { start, end };
}

export function payoutWindowForPeriodEnd(periodEnd: Date) {
  const start = new Date(Date.UTC(periodEnd.getUTCFullYear(), periodEnd.getUTCMonth() + 1, 10, 0, 0, 0, 0));
  const end = new Date(Date.UTC(periodEnd.getUTCFullYear(), periodEnd.getUTCMonth() + 1, 15, 23, 59, 59, 999));
  return { start, end };
}

export function nextPayoutWindow(date: Date) {
  const currentMonthWindow = payoutWindowForPeriodEnd(new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 0, 23, 59, 59, 999)));
  if (date <= currentMonthWindow.end) return currentMonthWindow;
  return payoutWindowForPeriodEnd(monthPeriod(date).end);
}

function buildDailyCommissionSeries(commissions: Array<{ amount: Prisma.Decimal; createdAt: Date }>, start: Date, end: Date) {
  const byDay = new Map<string, number>();
  const cursor = new Date(start);
  while (cursor <= end) {
    byDay.set(dayKey(cursor), 0);
    cursor.setDate(cursor.getDate() + 1);
  }

  for (const commission of commissions) {
    const key = dayKey(commission.createdAt);
    byDay.set(key, (byDay.get(key) ?? 0) + Number(commission.amount));
  }

  return [...byDay.entries()].map(([date, amount]) => ({ date, amount }));
}

function buildDailyActivitySeries(
  clicks: Array<{ createdAt: Date }>,
  referrals: Array<{ registeredAt: Date; ftdAt: Date | null; cpaQualifiedAt: Date | null; rewardEligible: boolean }>,
  start: Date,
  end: Date
) {
  const byDay = new Map<string, { date: string; clicks: number; registrations: number; ftd: number; cpa: number; rejectedGeo: number }>();
  const cursor = new Date(start);
  while (cursor <= end) {
    const date = dayKey(cursor);
    byDay.set(date, { date, clicks: 0, registrations: 0, ftd: 0, cpa: 0, rejectedGeo: 0 });
    cursor.setDate(cursor.getDate() + 1);
  }

  for (const click of clicks) {
    const item = byDay.get(dayKey(click.createdAt));
    if (item) item.clicks += 1;
  }

  for (const referral of referrals) {
    const registration = byDay.get(dayKey(referral.registeredAt));
    if (registration) {
      if (referral.rewardEligible) registration.registrations += 1;
      else registration.rejectedGeo += 1;
    }
    if (referral.rewardEligible && referral.ftdAt) {
      const ftd = byDay.get(dayKey(referral.ftdAt));
      if (ftd) ftd.ftd += 1;
    }
    if (referral.rewardEligible && referral.cpaQualifiedAt) {
      const cpa = byDay.get(dayKey(referral.cpaQualifiedAt));
      if (cpa) cpa.cpa += 1;
    }
  }

  return [...byDay.values()];
}

function dayKey(date: Date) {
  return date.toISOString().slice(0, 10);
}

function daysAgo(date: Date, days: number) {
  const next = new Date(date);
  next.setDate(next.getDate() - days);
  return next;
}

function startOfDay(date: Date) {
  const next = new Date(date);
  next.setHours(0, 0, 0, 0);
  return next;
}

function endOfDay(date: Date) {
  const next = new Date(date);
  next.setHours(23, 59, 59, 999);
  return next;
}

function normalizeRange(start: Date, end: Date, label: string) {
  const safeStart = Number.isNaN(start.getTime()) ? startOfDay(daysAgo(new Date(), 29)) : start;
  const safeEnd = Number.isNaN(end.getTime()) ? new Date() : end;
  if (safeStart <= safeEnd) return { start: safeStart, end: safeEnd, label };
  return { start: startOfDay(safeEnd), end: endOfDay(safeStart), label };
}

function normalizeCountry(country?: string | null) {
  return country?.trim().toUpperCase().slice(0, 2) || null;
}

function normalizeCountries(countries?: string[] | null) {
  return [...new Set((countries ?? []).map(normalizeCountry).filter((country): country is string => Boolean(country)))];
}

function isCountryAllowed(countries: string[], country: string | null) {
  const normalizedCountries = normalizeCountries(countries);
  if (!normalizedCountries.length) return true;
  if (!country) return false;
  return normalizedCountries.includes(country);
}

function withinCpaWindow(registeredAt: Date, depositAt: Date, days: number) {
  const deadline = new Date(registeredAt);
  deadline.setDate(deadline.getDate() + days);
  deadline.setHours(23, 59, 59, 999);
  return depositAt <= deadline;
}

function serializeAccount(account: {
  id: string;
  publicId: string;
  displayName: string;
  companyName: string | null;
  website: string | null;
  country: string;
  status: AffiliateAccountStatus;
  payoutMethod: string | null;
  createdAt: Date;
  approvedAt: Date | null;
}) {
  return {
    id: account.id,
    publicId: account.publicId,
    displayName: account.displayName,
    companyName: account.companyName,
    website: account.website,
    country: account.country,
    status: account.status,
    payoutMethod: account.payoutMethod,
    createdAt: account.createdAt.toISOString(),
    approvedAt: account.approvedAt?.toISOString() ?? null
  };
}

function serializeOffer(offer: {
  id: string;
  code: string;
  name: string;
  type: AffiliateOfferType;
  status: AffiliateOfferStatus;
  currency: string;
  cpaAmount: Prisma.Decimal | null;
  revenueShareBps: number | null;
  hybridCpaAmount: Prisma.Decimal | null;
  hybridRevenueShareBps: number | null;
  minFtdAmount: Prisma.Decimal;
  cookieDays: number;
  terms: string;
}) {
  return {
    id: offer.id,
    code: offer.code,
    name: offer.name,
    type: offer.type,
    status: offer.status,
    currency: offer.currency,
    cpaAmount: offer.cpaAmount?.toString() ?? null,
    revenueShareBps: offer.revenueShareBps,
    hybridCpaAmount: offer.hybridCpaAmount?.toString() ?? null,
    hybridRevenueShareBps: offer.hybridRevenueShareBps,
    minFtdAmount: offer.minFtdAmount.toString(),
    cookieDays: offer.cookieDays,
    terms: offer.terms
  };
}

function serializeLink(link: {
  id: string;
  code: string;
  label: string;
  landingPath: string;
  status: string;
  rewardType: AffiliateOfferType;
  cpaAmount: Prisma.Decimal | null;
  revenueShareBps: number | null;
  minCpaDepositAmount: Prisma.Decimal;
  cpaValidationMode: AffiliateCpaValidationMode;
  cpaWindowDays: number | null;
  allowedCountries: string[];
  terms: string | null;
  createdAt: Date;
}) {
  return {
    id: link.id,
    code: link.code,
    label: link.label,
    landingPath: link.landingPath,
    status: link.status,
    rewardType: link.rewardType,
    cpaAmount: link.cpaAmount?.toString() ?? null,
    revenueShareBps: link.revenueShareBps,
    minCpaDepositAmount: link.minCpaDepositAmount.toString(),
    cpaValidationMode: link.cpaValidationMode,
    cpaWindowDays: link.cpaWindowDays,
    allowedCountries: link.allowedCountries,
    terms: link.terms,
    url: `/affiliation/r/${link.code}`,
    createdAt: link.createdAt.toISOString()
  };
}

function serializePayout(payout: {
  id: string;
  periodStart: Date;
  periodEnd: Date;
  currency: string;
  amount: Prisma.Decimal;
  status: string;
  requestedAt: Date | null;
  approvedAt: Date | null;
  paidAt: Date | null;
  paymentMethod: string | null;
  paymentReference: string | null;
}) {
  return {
    id: payout.id,
    periodStart: payout.periodStart.toISOString(),
    periodEnd: payout.periodEnd.toISOString(),
    currency: payout.currency,
    amount: payout.amount.toString(),
    status: payout.status,
    requestedAt: payout.requestedAt?.toISOString() ?? null,
    approvedAt: payout.approvedAt?.toISOString() ?? null,
    paidAt: payout.paidAt?.toISOString() ?? null,
    paymentMethod: payout.paymentMethod,
    paymentReference: payout.paymentReference
  };
}

async function uniqueAffiliateLinkCode() {
  for (let attempt = 0; attempt < 8; attempt += 1) {
    const code = affiliateLinkCode();
    const existing = await prisma.affiliateLink.findUnique({ where: { code } });
    if (!existing) return code;
  }
  throw new AppError("Unable to allocate affiliate link", 503, "AFFILIATE_LINK_ALLOCATION_FAILED");
}
