import { scryptSync, randomBytes, timingSafeEqual } from 'node:crypto';
import type { PrismaService } from './prisma.service';
import { currentShop } from './shop-context';

/**
 * Resolve the merchant for the current request. In the embedded app the shop
 * comes from the verified session token (shop-context); locally / on public
 * routes it falls back to DEMO_SHOP so the dev experience keeps working.
 * Get-or-creates so a freshly-installed shop always has a merchant row.
 */
export async function getMerchant(prisma: PrismaService) {
  const shop = currentShop() || process.env.DEMO_SHOP || 'partner-nook-dev.myshopify.com';
  const existing = await prisma.merchant.findUnique({ where: { shop } });
  if (existing) return existing;
  return prisma.merchant.create({
    data: { shop, programName: shop.replace('.myshopify.com', ''), minPayout: 500 },
  });
}

/**
 * Split an affiliate's balance into payable vs held. Commission from orders
 * newer than `holdDays` is "held" (not yet payable) — a refund window. When
 * holdDays is 0 everything is instantly payable.
 */
export async function payableBalance(
  prisma: PrismaService,
  affiliate: { id: string; balance: unknown },
  holdDays: number,
): Promise<{ payable: number; held: number }> {
  const balance = Number(affiliate.balance) || 0;
  if (!holdDays || holdDays <= 0) return { payable: balance, held: 0 };
  const cutoff = new Date(Date.now() - holdDays * 86400000);
  const agg = await prisma.referralOrder.aggregate({
    where: { affiliateId: affiliate.id, status: 'APPROVED', createdAt: { gt: cutoff } },
    _sum: { commission: true },
  });
  const held = Number(agg._sum.commission || 0);
  return { payable: Math.max(0, balance - held), held: Math.min(held, balance) };
}

/** Mask an email for safe logging: "priya@gmail.com" -> "p***@g***". */
export function maskEmail(email?: string | null): string | undefined {
  if (!email) return undefined;
  const [user, domain] = email.split('@');
  if (!domain) return 'invalid';
  return `${user.slice(0, 1)}***@${domain.slice(0, 1)}***`;
}

/**
 * Append an entry to the personal-data access audit trail. Records a MASKED
 * reference only (never raw PII) and never throws — auditing must not break the
 * request it is auditing.
 */
export async function logDataAccess(
  prisma: PrismaService,
  merchantId: string | null,
  action: string,
  subject?: string,
): Promise<void> {
  try {
    await prisma.dataAccessLog.create({ data: { merchantId, action, subject: subject ?? null } });
  } catch {
    /* swallow — the audit log is best-effort */
  }
}

/** Verify a Google reCAPTCHA token (v2 or v3). Returns true if it passes. */
export async function verifyRecaptcha(secret: string, token?: string): Promise<boolean> {
  if (!token) return false;
  try {
    const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ secret, response: token }).toString(),
    });
    const d = (await res.json()) as { success?: boolean; score?: number };
    // v3 returns a 0–1 score; v2 has none. Accept success + a non-bot score.
    return !!d.success && (d.score === undefined || d.score >= 0.5);
  } catch {
    return false;
  }
}

/** Generate a unique coupon/referral code from a name, e.g. "SARA47". */
export function genCode(name: string) {
  const base = (name.split(' ')[0] || 'AFF')
    .replace(/[^a-zA-Z]/g, '')
    .toUpperCase()
    .slice(0, 8);
  const n = Math.floor(Math.random() * 90 + 10);
  return `${base}${n}`;
}

export function hashPassword(pw: string) {
  const salt = randomBytes(16).toString('hex');
  const hash = scryptSync(pw, salt, 32).toString('hex');
  return `${salt}:${hash}`;
}

export function verifyPassword(pw: string, stored?: string | null) {
  if (!stored) return false;
  const [salt, hash] = stored.split(':');
  if (!salt || !hash) return false;
  const test = scryptSync(pw, salt, 32);
  const orig = Buffer.from(hash, 'hex');
  return test.length === orig.length && timingSafeEqual(test, orig);
}

// Lazily-built SMTP transporter (only when SMTP_* env is configured).
let mailTransport: import('nodemailer').Transporter | null = null;
let mailInit = false;
async function getTransport(): Promise<import('nodemailer').Transporter | null> {
  if (mailInit) return mailTransport;
  mailInit = true;
  if (!process.env.SMTP_HOST) return null;
  try {
    const nodemailer = await import('nodemailer');
    mailTransport = nodemailer.createTransport({
      host: process.env.SMTP_HOST,
      port: Number(process.env.SMTP_PORT || 587),
      secure: String(process.env.SMTP_SECURE || '') === 'true',
      auth: process.env.SMTP_USER ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } : undefined,
    });
  } catch {
    mailTransport = null;
  }
  return mailTransport;
}

/**
 * Send (and log) a transactional email, respecting the merchant's notification
 * toggles. Sends for real via SMTP when SMTP_HOST is set; otherwise it just logs
 * (dev fallback). Status on the log reflects whether it actually went out.
 */
export async function logEmail(
  prisma: PrismaService,
  merchantId: string,
  toEmail: string,
  type: string,
  subject: string,
  body?: string,
) {
  const m = await prisma.merchant.findUnique({ where: { id: merchantId }, select: { notifications: true } });
  const notif = (m?.notifications as Record<string, boolean> | null) || null;
  if (notif && notif[type] === false) return; // this notification is turned off

  let status = 'sent';
  const transport = await getTransport();
  if (transport) {
    try {
      await transport.sendMail({
        from: process.env.SMTP_FROM || process.env.SMTP_USER,
        to: toEmail,
        subject,
        text: body || subject,
      });
    } catch {
      status = 'failed';
    }
  }
  await prisma.emailLog.create({ data: { merchantId, toEmail, type, subject, status } });
}

type Tier = { minSales: number; maxSales: number | null; rate: number };

type CommissionInputs = {
  merchant: {
    commissionBase: 'SUBTOTAL' | 'TOTAL';
    defaultCommissionType: 'PERCENT' | 'FLAT';
    defaultCommissionValue: unknown;
  };
  affiliate: {
    commissionType: 'PERCENT' | 'FLAT' | null;
    commissionValue: unknown;
    group?: { type: 'PERCENT' | 'FLAT'; value: unknown; tiers?: unknown } | null;
  };
  subtotal: number;
  total: number;
  /** Affiliate's approved sales so far this month — used to pick a group tier. */
  salesThisMonth?: number;
};

/** Parse the group's `tiers` Json blob into a typed, safe list. */
function parseTiers(tiers: unknown): Tier[] {
  if (!Array.isArray(tiers)) return [];
  return tiers
    .filter((t): t is Record<string, unknown> => !!t && typeof t === 'object')
    .map((t) => ({
      minSales: Number(t.minSales) || 0,
      maxSales: t.maxSales == null ? null : Number(t.maxSales),
      rate: Number(t.rate) || 0,
    }));
}

/**
 * Resolve the commission for one order.
 * Priority: per-affiliate override -> group (tiered, else flat) -> merchant default.
 * A group's `tiers` (if defined) pick a percent rate by the affiliate's sales
 * count this month; otherwise the group's flat rate is used.
 * Base: subtotal or total per merchant setting.
 */
export function calcCommission(i: CommissionInputs) {
  let type = i.merchant.defaultCommissionType;
  let value = Number(i.merchant.defaultCommissionValue);
  let source = 'default';

  if (i.affiliate.group) {
    type = i.affiliate.group.type;
    value = Number(i.affiliate.group.value);
    source = 'group';

    // Tiered override within the group (based on this month's sales).
    const tiers = parseTiers(i.affiliate.group.tiers);
    if (tiers.length) {
      const sales = i.salesThisMonth ?? 0;
      const tier = tiers.find(
        (t) => sales >= t.minSales && (t.maxSales == null || sales <= t.maxSales),
      );
      if (tier) {
        type = 'PERCENT';
        value = tier.rate;
        source = 'group_tier';
      }
    }
  }

  if (i.affiliate.commissionType && i.affiliate.commissionValue != null) {
    type = i.affiliate.commissionType;
    value = Number(i.affiliate.commissionValue);
    source = 'affiliate';
  }

  const base = i.merchant.commissionBase === 'TOTAL' ? i.total : i.subtotal;
  const amount = type === 'PERCENT' ? (base * value) / 100 : value;
  return { commission: Math.round(amount * 100) / 100, type, value, source };
}
