import { audit, getSetting, setSetting, withTransaction } from './db.js';
import { postTransferInTx } from './ledger.js';
import { nowIso, uuid } from './utils.js';

const POLICIES = new Set(['fixed_at_deposit', 'launch_price', 'pro_rata_pool']);

function fail(code, message, status = 400, details) {
  throw Object.assign(new Error(message), { code, status, details });
}

export function amountBig(value, fallback = 0n) {
  try {
    const text = String(value ?? '').trim();
    if (!/^-?\d+$/.test(text)) return fallback;
    return BigInt(text);
  } catch { return fallback; }
}

export function tokenScale(db) {
  const decimals = Math.max(0, Math.min(36, Number(getSetting(db, 'project_token_decimals', 18)) || 18));
  return 10n ** BigInt(decimals);
}

export function formatBaseUnits(value, decimals = 18, maxFraction = 8) {
  const n = amountBig(value);
  const negative = n < 0n;
  const abs = negative ? -n : n;
  const scale = 10n ** BigInt(decimals);
  const whole = abs / scale;
  const fraction = String(abs % scale).padStart(decimals, '0').slice(0, maxFraction).replace(/0+$/, '');
  return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`;
}

function policy(db) {
  const value = String(getSetting(db, 'presale_allocation_policy', 'launch_price'));
  return POLICIES.has(value) ? value : 'launch_price';
}

function configBig(db, key, fallback) {
  const value = amountBig(getSetting(db, key, fallback), amountBig(fallback));
  if (value < 0n) fail('INVALID_PRESALE_CONFIG', `${key} negatif olamaz.`, 500);
  return value;
}

export function presaleConfig(db) {
  const latest = db.prepare("SELECT * FROM presale_allocation_snapshots WHERE status='finalized' ORDER BY version DESC LIMIT 1").get() || null;
  const totalPaid = Number(db.prepare(`SELECT COALESCE(SUM(p.usdt_micros),0) AS total
    FROM presale_purchases p JOIN payment_orders o ON o.id=p.order_id
    WHERE o.status='paid' AND p.status NOT IN ('cancelled','refunded')`).get().total || 0);
  return {
    policy: policy(db),
    totalSupplyTokenBase: configBig(db, 'token_total_supply_base_units', '10000000000000000000000000000').toString(),
    poolTokenBase: configBig(db, 'presale_pool_token_base_units', '2000000000000000000000000000').toString(),
    referencePriceUsdtMicros: Math.max(1, Number(getSetting(db, 'presale_reference_price_usdt_micros', 12500)) || 12500),
    launchPriceUsdtMicros: Math.max(1, Number(getSetting(db, 'presale_launch_price_usdt_micros', 30000)) || 30000),
    termsVersion: String(getSetting(db, 'presale_terms_version', '1.0')),
    autoFinalizeAtLaunch: Boolean(getSetting(db, 'presale_auto_finalize_at_launch', false)),
    locked: Boolean(getSetting(db, 'presale_settings_locked', false)),
    totalPaidUsdtMicros: totalPaid,
    latestFinalization: latest,
    decimals: Number(getSetting(db, 'project_token_decimals', 18)) || 18
  };
}

export function validatePresaleSettings(input = {}) {
  const nextPolicy = String(input.policy || 'launch_price');
  if (!POLICIES.has(nextPolicy)) fail('INVALID_PRESALE_POLICY', 'Geçersiz presale tahsisat politikası.', 400);
  const total = amountBig(input.totalSupplyTokenBase);
  const pool = amountBig(input.poolTokenBase);
  if (total <= 0n || pool <= 0n || pool > total) fail('INVALID_PRESALE_SUPPLY', 'Presale havuzu toplam arzdan büyük olamaz.', 400);
  const reference = Number(input.referencePriceUsdtMicros);
  const launch = Number(input.launchPriceUsdtMicros);
  if (!Number.isSafeInteger(reference) || reference <= 0 || !Number.isSafeInteger(launch) || launch <= 0) fail('INVALID_PRESALE_PRICE', 'Presale fiyatları pozitif tam sayı mikro-USDT olmalıdır.', 400);
  return { nextPolicy, total, pool, reference, launch };
}

export function updatePresaleCoreSettings(db, input, actor = 'admin', ip = '') {
  if (Boolean(getSetting(db, 'presale_settings_locked', false))) fail('PRESALE_SETTINGS_LOCKED', 'Presale finalizasyonundan sonra arz ve fiyat ayarları kilitlenmiştir.', 409);
  const values = validatePresaleSettings(input);
  const before = presaleConfig(db);
  setSetting(db, 'presale_allocation_policy', values.nextPolicy, actor, ip);
  setSetting(db, 'token_total_supply_base_units', values.total.toString(), actor, ip);
  setSetting(db, 'presale_pool_token_base_units', values.pool.toString(), actor, ip);
  setSetting(db, 'presale_reference_price_usdt_micros', values.reference, actor, ip);
  setSetting(db, 'presale_launch_price_usdt_micros', values.launch, actor, ip);
  if (input.termsVersion != null) setSetting(db, 'presale_terms_version', String(input.termsVersion).slice(0, 30), actor, ip);
  if (input.autoFinalizeAtLaunch != null) setSetting(db, 'presale_auto_finalize_at_launch', Boolean(input.autoFinalizeAtLaunch), actor, ip);
  const after = presaleConfig(db);
  audit(db, { actorType: 'admin', actorId: actor, action: 'presale.core_settings.update', targetType: 'presale', targetId: 'core', before, after, ip });
  return after;
}

export function estimatePresaleTokens(db, { usdtMicros, bonusBps = 0, priceSnapshotUsdtMicros = 0, allocationPolicy = policy(db) }) {
  const deposit = BigInt(Math.max(0, Number(usdtMicros) || 0));
  const weighted = deposit * BigInt(10000 + Math.max(0, Number(bonusBps) || 0)) / 10000n;
  const cfg = presaleConfig(db);
  const price = BigInt(allocationPolicy === 'fixed_at_deposit' && priceSnapshotUsdtMicros > 0 ? priceSnapshotUsdtMicros : cfg.launchPriceUsdtMicros);
  const estimate = price > 0n ? weighted * tokenScale(db) / price : 0n;
  return { weightedUsdtMicros: Number(weighted), estimatedTokenBase: estimate.toString(), policy: allocationPolicy, priceUsdtMicros: Number(price) };
}

export function markPresalePaymentFundedInTx(db, order, actor = 'system') {
  const purchase = db.prepare('SELECT * FROM presale_purchases WHERE order_id=?').get(order.id);
  if (!purchase) fail('PRESALE_PURCHASE_NOT_FOUND', 'Presale kaydı bulunamadı.', 404);
  if (purchase.status === 'allocated') return { purchase, duplicate: true };
  const stage = db.prepare('SELECT * FROM presale_stages WHERE id=?').get(purchase.stage_id);
  const selectedPolicy = purchase.pricing_policy || policy(db);
  const estimate = estimatePresaleTokens(db, {
    usdtMicros: purchase.usdt_micros,
    bonusBps: stage?.bonus_bps || 0,
    priceSnapshotUsdtMicros: purchase.price_snapshot_usdt_micros || stage?.price_usdt_micros || 0,
    allocationPolicy: selectedPolicy
  });
  postTransferInTx(db, {
    fromAccountId: 'sys-usdt-issuance',
    toAccountId: 'sys-usdt-presale-revenue',
    amount: purchase.usdt_micros,
    type: 'presale_usdt_payment',
    ref: purchase.id,
    idempotencyKey: `ledger:presale-usdt:${purchase.id}`,
    metadata: { orderId: order.id, actor, policy: selectedPolicy }
  });
  // The legacy table CHECK constraint uses `pending` for paid-but-not-finalized
  // purchases. Payment truth is held by payment_orders.status='paid'. Keeping
  // the purchase pending avoids a destructive table rebuild during upgrades.
  db.prepare(`UPDATE presale_purchases SET status='pending',pricing_policy=?,price_snapshot_usdt_micros=?,estimated_token_base=?,weighted_usdt_micros=? WHERE id=?`)
    .run(selectedPolicy, estimate.priceUsdtMicros, estimate.estimatedTokenBase, estimate.weightedUsdtMicros, purchase.id);
  return { purchase: db.prepare('SELECT * FROM presale_purchases WHERE id=?').get(purchase.id), funded: true };
}

function fundedRows(db) {
  return db.prepare(`SELECT p.*,o.status AS payment_status,s.name AS stage_name,s.bonus_bps AS stage_bonus_bps,s.price_usdt_micros AS stage_price_usdt_micros,u.username,u.display_name,u.login_id
    FROM presale_purchases p
    JOIN payment_orders o ON o.id=p.order_id
    JOIN presale_stages s ON s.id=p.stage_id
    JOIN users u ON u.id=p.user_id
    WHERE o.status='paid' AND p.status IN ('funded','pending') AND p.refund_status='none'
    ORDER BY p.created_at,p.id`).all();
}

function allocateLargestRemainder(rows, pool, numeratorOf) {
  const total = rows.reduce((sum, row) => sum + numeratorOf(row), 0n);
  if (total <= 0n) return rows.map((row) => ({ row, amount: 0n, remainder: 0n }));
  const allocations = rows.map((row) => {
    const numerator = numeratorOf(row);
    const product = pool * numerator;
    return { row, amount: product / total, remainder: product % total };
  });
  let assigned = allocations.reduce((sum, item) => sum + item.amount, 0n);
  let left = pool - assigned;
  const ordered = [...allocations].sort((a, b) => a.remainder === b.remainder ? String(a.row.id).localeCompare(String(b.row.id)) : (a.remainder > b.remainder ? -1 : 1));
  let index = 0;
  while (left > 0n && ordered.length) {
    ordered[index % ordered.length].amount += 1n;
    left -= 1n;
    index += 1;
  }
  return allocations;
}

export function previewPresaleAllocation(db) {
  const cfg = presaleConfig(db);
  const rows = fundedRows(db);
  const pool = amountBig(cfg.poolTokenBase);
  const scale = tokenScale(db);
  let allocations;
  if (cfg.policy === 'pro_rata_pool') {
    allocations = allocateLargestRemainder(rows, pool, (row) => BigInt(row.weighted_usdt_micros || row.usdt_micros));
  } else {
    const raw = rows.map((row) => {
      const price = cfg.policy === 'fixed_at_deposit' ? BigInt(row.price_snapshot_usdt_micros || row.stage_price_usdt_micros || cfg.referencePriceUsdtMicros) : BigInt(cfg.launchPriceUsdtMicros);
      const weighted = BigInt(row.weighted_usdt_micros || row.usdt_micros);
      return { row, requested: price > 0n ? weighted * scale / price : 0n };
    });
    const requestedTotal = raw.reduce((sum, item) => sum + item.requested, 0n);
    allocations = requestedTotal <= pool
      ? raw.map((item) => ({ row: item.row, amount: item.requested, remainder: 0n }))
      : allocateLargestRemainder(rows, pool, (row) => {
          const found = raw.find((item) => item.row.id === row.id);
          return found?.requested || 0n;
        });
  }
  const totalToken = allocations.reduce((sum, item) => sum + item.amount, 0n);
  const totalPaid = rows.reduce((sum, row) => sum + Number(row.usdt_micros || 0), 0);
  const totalWeighted = rows.reduce((sum, row) => sum + Number(row.weighted_usdt_micros || row.usdt_micros || 0), 0);
  return {
    policy: cfg.policy,
    poolTokenBase: pool.toString(),
    totalPaidUsdtMicros: totalPaid,
    totalWeightedUsdtMicros: totalWeighted,
    totalTokenBase: totalToken.toString(),
    purchaseCount: rows.length,
    rows: allocations.map((item) => ({
      purchaseId: item.row.id,
      userId: item.row.user_id,
      username: item.row.username,
      displayName: item.row.display_name,
      loginId: item.row.login_id,
      depositUsdtMicros: item.row.usdt_micros,
      weightedUsdtMicros: item.row.weighted_usdt_micros || item.row.usdt_micros,
      tokenBase: item.amount.toString(),
      tokenDisplay: formatBaseUnits(item.amount, cfg.decimals, 8),
      stageId: item.row.stage_id,
      stageName: item.row.stage_name
    }))
  };
}

export function createPresaleSnapshot(db, actor = 'admin', ip = '') {
  return withTransaction(db, () => {
    if (Boolean(getSetting(db, 'presale_settings_locked', false))) fail('PRESALE_ALREADY_FINALIZED', 'Presale daha önce kesinleştirilmiş.', 409);
    const preview = previewPresaleAllocation(db);
    if (!preview.purchaseCount) fail('NO_FUNDED_PRESALE', 'Kesinleştirilecek onaylı presale deposit bulunmuyor.', 409);
    const version = Number(db.prepare('SELECT COALESCE(MAX(version),0)+1 AS next FROM presale_allocation_snapshots').get().next);
    const id = uuid();
    const now = nowIso();
    db.prepare(`INSERT INTO presale_allocation_snapshots(id,version,policy,launch_price_usdt_micros,pool_token_base,total_paid_usdt_micros,total_weighted_usdt_micros,total_token_base,purchase_count,config_json,status,created_by,created_at)
      VALUES(?,?,?,?,?,?,?,?,?,?,'preview',?,?)`)
      .run(id, version, preview.policy, Number(getSetting(db, 'presale_launch_price_usdt_micros', 30000)), preview.poolTokenBase, preview.totalPaidUsdtMicros, preview.totalWeightedUsdtMicros, preview.totalTokenBase, preview.purchaseCount, JSON.stringify(preview.rows), actor, now);
    const after = db.prepare('SELECT * FROM presale_allocation_snapshots WHERE id=?').get(id);
    audit(db, { actorType: 'admin', actorId: actor, action: 'presale.snapshot.create', targetType: 'presale_snapshot', targetId: id, before: null, after, ip });
    return { snapshot: after, preview };
  });
}

export function approvePresaleSnapshot(db, snapshotId, actor = 'admin', ip = '', note = '') {
  return withTransaction(db, () => {
    const snapshot = db.prepare('SELECT * FROM presale_allocation_snapshots WHERE id=?').get(snapshotId);
    if (!snapshot) fail('PRESALE_SNAPSHOT_NOT_FOUND', 'Presale ön izlemesi bulunamadı.', 404);
    if (snapshot.status === 'finalized') return { snapshot, finalized: true };
    if (!['preview','pending_approval'].includes(snapshot.status)) fail('PRESALE_SNAPSHOT_NOT_APPROVABLE', 'Bu ön izleme onaylanamaz.', 409);
    db.prepare(`INSERT OR IGNORE INTO presale_finalization_approvals(id,snapshot_id,actor_id,decision,note,created_at) VALUES(?,?,?,?,?,?)`)
      .run(uuid(), snapshotId, actor, 'approve', String(note || '').slice(0, 500), nowIso());
    const approvals = Number(db.prepare("SELECT COUNT(*) AS count FROM presale_finalization_approvals WHERE snapshot_id=? AND decision='approve'").get(snapshotId).count);
    const required = Boolean(getSetting(db, 'presale_finalization_requires_two_approvals', false)) ? 2 : 1;
    db.prepare("UPDATE presale_allocation_snapshots SET status='pending_approval',approved_by=?,approved_at=? WHERE id=?")
      .run(approvals >= required ? actor : null, approvals >= required ? nowIso() : null, snapshotId);
    const updated = db.prepare('SELECT * FROM presale_allocation_snapshots WHERE id=?').get(snapshotId);
    audit(db, { actorType: 'admin', actorId: actor, action: 'presale.snapshot.approve', targetType: 'presale_snapshot', targetId: snapshotId, before: snapshot, after: updated, ip });
    return { snapshot: updated, approvals, required, ready: approvals >= required };
  });
}

export function finalizePresaleAllocation(db, snapshotId, actor = 'admin', ip = '') {
  return withTransaction(db, () => {
    const snapshot = db.prepare('SELECT * FROM presale_allocation_snapshots WHERE id=?').get(snapshotId);
    if (!snapshot) fail('PRESALE_SNAPSHOT_NOT_FOUND', 'Presale ön izlemesi bulunamadı.', 404);
    if (snapshot.status === 'finalized') return { snapshot, duplicate: true };
    const approvals = Number(db.prepare("SELECT COUNT(*) AS count FROM presale_finalization_approvals WHERE snapshot_id=? AND decision='approve'").get(snapshotId).count);
    const required = Boolean(getSetting(db, 'presale_finalization_requires_two_approvals', false)) ? 2 : 1;
    if (approvals < required) fail('PRESALE_APPROVAL_REQUIRED', `Kesinleştirme için ${required} ayrı yönetici onayı gerekir.`, 409, { approvals, required });
    const rows = JSON.parse(snapshot.config_json || '[]');
    const now = nowIso();
    const update = db.prepare(`UPDATE presale_purchases SET status='allocated',final_token_base=?,allocation_version=?,finalized_at=?,allocated_at=? WHERE id=? AND status IN ('funded','pending')`);
    const stageTotals = new Map();
    for (const row of rows) {
      const result = update.run(String(row.tokenBase), snapshot.version, now, now, row.purchaseId);
      if (result.changes) stageTotals.set(row.stageId, (stageTotals.get(row.stageId) || 0n) + amountBig(row.tokenBase));
    }
    for (const [stageId, amount] of stageTotals) {
      const mirror = amount <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(amount) : Number.MAX_SAFE_INTEGER;
      db.prepare('UPDATE presale_stages SET sold_token_base=?,updated_at=? WHERE id=?').run(mirror, now, stageId);
    }
    db.prepare("UPDATE presale_allocation_snapshots SET status='finalized',finalized_at=?,approved_by=COALESCE(approved_by,?),approved_at=COALESCE(approved_at,?) WHERE id=?")
      .run(now, actor, now, snapshotId);
    setSetting(db, 'presale_settings_locked', true, actor, ip);
    setSetting(db, 'presale_finalized_snapshot_id', snapshotId, actor, ip);
    const after = db.prepare('SELECT * FROM presale_allocation_snapshots WHERE id=?').get(snapshotId);
    audit(db, { actorType: 'admin', actorId: actor, action: 'presale.finalize', targetType: 'presale_snapshot', targetId: snapshotId, before: snapshot, after, ip });
    return { snapshot: after, allocations: rows, duplicate: false };
  });
}

export function presaleAdminOverview(db) {
  const cfg = presaleConfig(db);
  const snapshots = db.prepare(`SELECT s.*,(SELECT COUNT(*) FROM presale_finalization_approvals a WHERE a.snapshot_id=s.id AND a.decision='approve') AS approval_count FROM presale_allocation_snapshots s ORDER BY version DESC LIMIT 30`).all();
  const contributions = db.prepare(`SELECT p.*,u.username,u.display_name,u.login_id,o.status AS payment_status,s.name AS stage_name
    FROM presale_purchases p JOIN users u ON u.id=p.user_id JOIN payment_orders o ON o.id=p.order_id JOIN presale_stages s ON s.id=p.stage_id
    ORDER BY p.created_at DESC LIMIT 500`).all();
  return { config: cfg, preview: cfg.locked ? null : previewPresaleAllocation(db), snapshots, contributions };
}

export function presaleUserSummary(db, userId) {
  const cfg = presaleConfig(db);
  const rawRows = db.prepare(`SELECT p.*,o.status AS payment_status,s.name AS stage_name,s.bonus_bps AS stage_bonus_bps
    FROM presale_purchases p JOIN payment_orders o ON o.id=p.order_id JOIN presale_stages s ON s.id=p.stage_id
    WHERE p.user_id=? ORDER BY p.created_at DESC`).all(userId);

  // The deposit is the immutable source of truth. Until allocation is finalized,
  // estimates are recalculated from the current price/pool/policy so an admin
  // price or supply change is reflected immediately in every buyer's wallet.
  let previewByPurchase = new Map();
  if (!cfg.locked) {
    try {
      previewByPurchase = new Map(previewPresaleAllocation(db).rows.map((row) => [row.purchaseId, row]));
    } catch {
      previewByPurchase = new Map();
    }
  }
  const rows = rawRows.map((row) => {
    const preview = previewByPurchase.get(row.id);
    const currentEstimate = amountBig(row.final_token_base) > 0n
      ? amountBig(row.final_token_base)
      : amountBig(preview?.tokenBase ?? row.estimated_token_base);
    return {
      ...row,
      current_estimated_token_base: currentEstimate.toString(),
      current_estimated_token_display: formatBaseUnits(currentEstimate, cfg.decimals, 8)
    };
  });
  const eligible = rows.filter((row) => row.payment_status === 'paid' && !['cancelled','refunded'].includes(row.status));
  const totalDeposit = eligible.reduce((sum, row) => sum + Number(row.usdt_micros || 0), 0);
  const estimated = eligible.reduce((sum, row) => sum + amountBig(row.current_estimated_token_base), 0n);
  const finalized = rows.reduce((sum, row) => sum + amountBig(row.final_token_base), 0n);
  return {
    policy: cfg.policy,
    totalDepositUsdtMicros: totalDeposit,
    estimatedTokenBase: estimated.toString(),
    finalizedTokenBase: finalized.toString(),
    estimatedTokenDisplay: formatBaseUnits(estimated, cfg.decimals, 8),
    finalizedTokenDisplay: formatBaseUnits(finalized, cfg.decimals, 8),
    locked: cfg.locked,
    purchases: rows
  };
}

export function maybeFinalizeAtLaunch(db, actor = 'launch-worker') {
  if (!Boolean(getSetting(db, 'presale_auto_finalize_at_launch', false))) return { skipped: 'disabled' };
  if (Boolean(getSetting(db, 'presale_settings_locked', false))) return { skipped: 'already_finalized' };
  if (Date.now() < new Date(getSetting(db, 'launch_at', 0)).getTime()) return { skipped: 'before_launch' };
  const created = createPresaleSnapshot(db, actor, '');
  approvePresaleSnapshot(db, created.snapshot.id, actor, '', 'Otomatik lansman onayı');
  return finalizePresaleAllocation(db, created.snapshot.id, actor, '');
}
