import crypto from 'node:crypto';
import { getSetting, audit, withTransaction } from './db.js';
import { config } from './config.js';
import { getSecret } from './secrets.js';
import { amountBig, maybeFinalizeAtLaunch, tokenScale } from './presale.js';
import { postTransferInTx, userAccountId } from './ledger.js';
import { nowIso, nowSeconds, sanitizeText, uuid } from './utils.js';

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

function contractAddress(db) {
  return String(getSetting(db, 'distribution_contract_address', '') || getSetting(db, 'project_token_contract', '')).trim().toLowerCase();
}

function eligibleWallet(db, userId) {
  const confirmed = Boolean(getSetting(db, 'distribution_requires_confirmed_wallet', true));
  const where = confirmed ? "AND verification_status IN ('email_confirmed','signature_verified','admin_verified')" : '';
  return db.prepare(`SELECT * FROM user_wallets WHERE user_id=? AND wallet_type='external_mainnet' AND status='active' AND available_at<=? ${where}
    ORDER BY CASE verification_status WHEN 'signature_verified' THEN 1 WHEN 'admin_verified' THEN 2 WHEN 'email_confirmed' THEN 3 ELSE 4 END,created_at DESC LIMIT 1`)
    .get(userId, nowSeconds()) || null;
}

function snapshotAllocations(db, snapshotId) {
  const snapshot = db.prepare("SELECT * FROM presale_allocation_snapshots WHERE id=? AND status='finalized'").get(snapshotId);
  if (!snapshot) fail('PRESALE_SNAPSHOT_NOT_FINALIZED', 'Kesinleşmiş presale snapshot bulunamadı.', 409);
  const rows = JSON.parse(snapshot.config_json || '[]');
  const grouped = new Map();
  for (const row of rows) grouped.set(row.userId, (grouped.get(row.userId) || 0n) + amountBig(row.tokenBase));
  return { snapshot, grouped };
}

export function distributionOverview(db) {
  const latest = db.prepare("SELECT * FROM presale_allocation_snapshots WHERE status='finalized' ORDER BY version DESC LIMIT 1").get() || null;
  return {
    settings: {
      enabled: Boolean(getSetting(db, 'distribution_enabled', false)),
      autoStart: Boolean(getSetting(db, 'distribution_auto_start', false)),
      mode: String(getSetting(db, 'distribution_mode', 'auto_contract')),
      contractAddress: contractAddress(db),
      signerMode: String(getSetting(db, 'distribution_signer_mode', 'webhook')),
      signerUrl: String(getSetting(db, 'distribution_signer_url', '')),
      batchSize: Number(getSetting(db, 'distribution_batch_size', 100)) || 100,
      confirmationsRequired: Number(getSetting(db, 'distribution_confirmations_required', 12)) || 12,
      latestSnapshot: latest
    },
    campaigns: db.prepare('SELECT * FROM distribution_campaigns ORDER BY created_at DESC LIMIT 100').all(),
    recentItems: db.prepare(`SELECT i.*,u.username,u.display_name,u.login_id FROM distribution_items i JOIN users u ON u.id=i.user_id ORDER BY i.created_at DESC LIMIT 300`).all()
  };
}

export function createDistributionCampaign(db, input = {}, actor = 'admin', ip = '') {
  return withTransaction(db, () => {
    if (!Boolean(getSetting(db, 'distribution_enabled', false))) fail('DISTRIBUTION_DISABLED', 'Lansman dağıtımı kapalıdır.', 403);
    const snapshot = input.snapshotId
      ? db.prepare("SELECT * FROM presale_allocation_snapshots WHERE id=? AND status='finalized'").get(input.snapshotId)
      : db.prepare("SELECT * FROM presale_allocation_snapshots WHERE status='finalized' ORDER BY version DESC LIMIT 1").get();
    if (!snapshot) fail('PRESALE_FINALIZATION_REQUIRED', 'Dağıtımdan önce presale kesinleştirilmelidir.', 409);
    const existing = db.prepare("SELECT * FROM distribution_campaigns WHERE snapshot_id=? AND status<>'cancelled' ORDER BY created_at DESC LIMIT 1").get(snapshot.id);
    if (existing) return { campaign: existing, items: db.prepare('SELECT * FROM distribution_items WHERE campaign_id=? ORDER BY created_at').all(existing.id), duplicate: true };

    const currentContract = contractAddress(db);
    const requestedMode = String(input.mode || getSetting(db, 'distribution_mode', 'auto_contract'));
    const mode = currentContract && requestedMode === 'auto_contract' ? 'auto_contract' : 'manual_withdrawal';
    const { grouped: presaleGrouped } = snapshotAllocations(db, snapshot.id);
    const accumulatedRows = db.prepare(`SELECT user_id,balance FROM ledger_accounts
      WHERE asset='OC_TOKEN' AND user_id IS NOT NULL AND balance>0 ORDER BY user_id`).all();
    const accumulated = new Map(accumulatedRows.map((row) => [row.user_id, Number(row.balance || 0)]));
    const allUsers = new Set([...presaleGrouped.keys(), ...accumulated.keys()]);
    if (!allUsers.size) fail('NO_DISTRIBUTABLE_TOKENS', 'Dağıtılacak presale veya birikmiş OC_TOKEN bulunmuyor.', 409);

    const scale = tokenScale(db);
    const id = uuid();
    const now = nowIso();
    let total = 0n;
    const items = [];
    for (const userId of allUsers) {
      const presaleBase = presaleGrouped.get(userId) || 0n;
      const gameWhole = accumulated.get(userId) || 0;
      const gameBase = BigInt(gameWhole) * scale;
      const amount = presaleBase + gameBase;
      if (amount <= 0n) continue;
      total += amount;
      items.push({ userId, presaleBase, gameWhole, amount });
    }
    if (!items.length) fail('NO_DISTRIBUTABLE_TOKENS', 'Dağıtılacak token miktarı bulunmuyor.', 409);

    db.prepare(`INSERT INTO distribution_campaigns(id,name,mode,network,chain_id,token_contract,signer_mode,snapshot_id,status,total_recipients,total_token_base,processed_recipients,created_by,created_at,updated_at)
      VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
      id,
      sanitizeText(input.name || `OnlyCoin Lansman Dağıtımı v${snapshot.version}`, 120),
      mode,
      sanitizeText(input.network || getSetting(db, 'wallet_mainnet_network_name', getSetting(db, 'usdt_network_name', 'EVM')), 80),
      Number(input.chainId || getSetting(db, 'wallet_mainnet_chain_id', getSetting(db, 'usdt_chain_id', 56))) || 56,
      currentContract,
      sanitizeText(input.signerMode || getSetting(db, 'distribution_signer_mode', 'webhook'), 40),
      snapshot.id,
      mode === 'manual_withdrawal' ? 'manual_open' : 'ready',
      items.length,
      total.toString(),
      0,
      actor,
      now,
      now
    );
    const insert = db.prepare(`INSERT INTO distribution_items(id,campaign_id,user_id,wallet_id,address,amount_token_base,presale_token_base,game_token_whole,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`);
    for (const item of items) {
      if (item.gameWhole > 0) {
        postTransferInTx(db, {
          fromAccountId: userAccountId(item.userId, 'OC_TOKEN'),
          toAccountId: 'sys-token-withdrawal-clearing',
          amount: item.gameWhole,
          type: 'launch_distribution_lock',
          ref: id,
          idempotencyKey: `distribution-lock:${id}:${item.userId}`,
          metadata: { campaignId: id, presaleTokenBase: item.presaleBase.toString(), gameTokenWhole: item.gameWhole }
        });
      }
      const wallet = eligibleWallet(db, item.userId);
      const status = wallet ? (mode === 'manual_withdrawal' ? 'manual' : 'ready') : 'waiting_wallet';
      insert.run(uuid(), id, item.userId, wallet?.id || null, wallet?.address || '', item.amount.toString(), item.presaleBase.toString(), item.gameWhole, status, now, now);
    }
    const campaign = db.prepare('SELECT * FROM distribution_campaigns WHERE id=?').get(id);
    audit(db, { actorType: 'admin', actorId: actor, action: 'distribution.campaign.create', targetType: 'distribution_campaign', targetId: id, before: null, after: { ...campaign, includedAccumulatedTokens: true }, ip });
    return { campaign, items: db.prepare('SELECT * FROM distribution_items WHERE campaign_id=? ORDER BY created_at').all(id), duplicate: false };
  });
}

async function externalSign(db, campaign, item) {
  const mode = campaign.signer_mode || getSetting(db, 'distribution_signer_mode', 'webhook');
  if (mode === 'test-signer' || mode === 'mock') {
    if (config.environment !== 'test') fail('SIGNER_NOT_CONFIGURED', 'Canlı dağıtım için harici signer/custody servisi yapılandırılmalıdır.', 503);
    return { txHash: `0x${crypto.createHash('sha256').update(`${campaign.id}:${item.id}:${item.address}:${item.amount_token_base}`).digest('hex')}`, receipt: { test: true } };
  }
  if (mode !== 'webhook') fail('SIGNER_NOT_CONFIGURED', 'Desteklenen signer modu yapılandırılmamış.', 503);
  const url = String(getSetting(db, 'distribution_signer_url', ''));
  const secret = getSecret(db, 'distribution_signer_secret', process.env.DISTRIBUTION_SIGNER_SECRET || '');
  if (!url || !secret) fail('SIGNER_NOT_CONFIGURED', 'Dağıtım signer URL veya secret bilgisi eksik.', 503);
  const payload = JSON.stringify({ campaignId: campaign.id, itemId: item.id, chainId: campaign.chain_id, contract: campaign.token_contract, recipient: item.address, amountBase: item.amount_token_base });
  const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OnlyCoin-Signature': signature }, body: payload, signal: AbortSignal.timeout(20000) });
  const data = await response.json().catch(() => ({}));
  if (!response.ok || !/^0x[0-9a-fA-F]{64}$/.test(String(data.txHash || ''))) fail('SIGNER_FAILED', 'Harici signer geçerli transaction hash döndürmedi.', 502, data);
  return { txHash: String(data.txHash).toLowerCase(), receipt: data };
}

export async function runDistributionCampaign(db, campaignId, actor = 'admin', ip = '') {
  const campaign = db.prepare('SELECT * FROM distribution_campaigns WHERE id=?').get(campaignId);
  if (!campaign) fail('DISTRIBUTION_CAMPAIGN_NOT_FOUND', 'Dağıtım kampanyası bulunamadı.', 404);
  if (campaign.mode !== 'auto_contract') fail('DISTRIBUTION_MANUAL_MODE', 'Bu kampanya manuel çekim modundadır.', 409);
  if (!campaign.token_contract) fail('TOKEN_CONTRACT_REQUIRED', 'Otomatik dağıtım için contract adresi gereklidir.', 409);
  if (!['ready','running','paused'].includes(campaign.status)) return { campaign, processed: 0 };
  const batchSize = Math.max(1, Math.min(1000, Number(getSetting(db, 'distribution_batch_size', 100)) || 100));
  const items = db.prepare("SELECT * FROM distribution_items WHERE campaign_id=? AND status='ready' ORDER BY created_at LIMIT ?").all(campaignId, batchSize);
  db.prepare("UPDATE distribution_campaigns SET status='running',started_at=COALESCE(started_at,?),updated_at=? WHERE id=?").run(nowIso(), nowIso(), campaignId);
  let processed = 0;
  for (const item of items) {
    try {
      const result = await externalSign(db, campaign, item);
      db.prepare("UPDATE distribution_items SET status='broadcast',tx_hash=?,error_text='',updated_at=? WHERE id=?").run(result.txHash, nowIso(), item.id);
      processed += 1;
    } catch (error) {
      db.prepare("UPDATE distribution_items SET status='failed',error_text=?,updated_at=? WHERE id=?").run(String(error.message || error).slice(0, 1000), nowIso(), item.id);
    }
  }
  const completedCount = Number(db.prepare("SELECT COUNT(*) AS count FROM distribution_items WHERE campaign_id=? AND status IN ('broadcast','confirmed')").get(campaignId).count);
  const waiting = Number(db.prepare("SELECT COUNT(*) AS count FROM distribution_items WHERE campaign_id=? AND status IN ('ready','waiting_wallet','broadcast','failed')").get(campaignId).count);
  db.prepare('UPDATE distribution_campaigns SET processed_recipients=?,updated_at=? WHERE id=?').run(completedCount, nowIso(), campaignId);
  if (!waiting) db.prepare("UPDATE distribution_campaigns SET status='completed',completed_at=?,updated_at=? WHERE id=?").run(nowIso(), nowIso(), campaignId);
  const after = db.prepare('SELECT * FROM distribution_campaigns WHERE id=?').get(campaignId);
  audit(db, { actorType: 'admin', actorId: actor, action: 'distribution.campaign.run', targetType: 'distribution_campaign', targetId: campaignId, before: campaign, after, ip });
  return { campaign: after, processed, items: db.prepare('SELECT * FROM distribution_items WHERE campaign_id=? ORDER BY created_at').all(campaignId) };
}

export function broadcastManualDistributionItem(db, itemId, input = {}, actor = 'admin', ip = '') {
  return withTransaction(db, () => {
    const before = db.prepare(`SELECT i.*,c.mode,c.chain_id,c.token_contract FROM distribution_items i
      JOIN distribution_campaigns c ON c.id=i.campaign_id WHERE i.id=?`).get(itemId);
    if (!before) fail('DISTRIBUTION_ITEM_NOT_FOUND', 'Dağıtım kaydı bulunamadı.', 404);
    if (before.status === 'broadcast' || before.status === 'confirmed') return { item: before, duplicate: true };
    if (before.status !== 'manual') fail('DISTRIBUTION_ITEM_NOT_MANUAL', 'Yalnızca manuel dağıtım kaydı broadcast edilebilir.', 409);
    if (!/^0x[0-9a-fA-F]{40}$/.test(String(before.address || ''))) fail('DISTRIBUTION_WALLET_REQUIRED', 'Doğrulanmış mainnet cüzdan adresi gereklidir.', 409);
    const txHash = String(input.txHash || '').trim().toLowerCase();
    if (!/^0x[0-9a-f]{64}$/.test(txHash)) fail('TX_HASH_INVALID', 'Geçerli transaction hash gereklidir.', 400);
    const duplicateHash = db.prepare('SELECT id FROM distribution_items WHERE tx_hash=? AND id<>?').get(txHash, itemId);
    if (duplicateHash) fail('TX_HASH_ALREADY_USED', 'Bu transaction hash başka bir dağıtımda kullanılmış.', 409);
    db.prepare("UPDATE distribution_items SET status='broadcast',tx_hash=?,error_text='',updated_at=? WHERE id=?").run(txHash, nowIso(), itemId);
    const after = db.prepare('SELECT * FROM distribution_items WHERE id=?').get(itemId);
    audit(db, { actorType: 'admin', actorId: actor, action: 'distribution.item.broadcast_manual', targetType: 'distribution_item', targetId: itemId, before, after, ip });
    return { item: after, duplicate: false };
  });
}

export function confirmDistributionItem(db, itemId, input = {}, actor = 'admin', ip = '') {
  return withTransaction(db, () => {
    const before = db.prepare('SELECT i.*,c.chain_id,c.token_contract,c.id AS campaign_ref FROM distribution_items i JOIN distribution_campaigns c ON c.id=i.campaign_id WHERE i.id=?').get(itemId);
    if (!before) fail('DISTRIBUTION_ITEM_NOT_FOUND', 'Dağıtım kaydı bulunamadı.', 404);
    if (before.status === 'confirmed') return { item: before, duplicate: true };
    if (before.status !== 'broadcast') fail('DISTRIBUTION_ITEM_NOT_BROADCAST', 'Yalnızca yayınlanmış işlem onaylanabilir.', 409);
    const txHash = String(input.txHash || before.tx_hash).toLowerCase();
    if (!/^0x[0-9a-f]{64}$/.test(txHash)) fail('TX_HASH_INVALID', 'Geçerli transaction hash gereklidir.', 400);
    const confirmations = Math.max(Number(input.confirmations || 0), Number(getSetting(db, 'distribution_confirmations_required', 12)) || 12);
    const receipt = {
      chainId: Number(input.chainId || before.chain_id),
      contract: String(input.contract || before.token_contract).toLowerCase(),
      recipient: String(input.recipient || before.address).toLowerCase(),
      amountBase: String(input.amountBase || before.amount_token_base),
      blockNumber: String(input.blockNumber || '')
    };
    if (receipt.chainId !== Number(before.chain_id) || receipt.contract !== String(before.token_contract).toLowerCase() || receipt.recipient !== String(before.address).toLowerCase() || amountBig(receipt.amountBase) !== amountBig(before.amount_token_base)) {
      fail('DISTRIBUTION_RECEIPT_MISMATCH', 'Blockchain receipt kampanya verileriyle eşleşmiyor.', 409, receipt);
    }
    db.prepare("UPDATE distribution_items SET status='confirmed',confirmations=?,block_number=?,updated_at=? WHERE id=?").run(confirmations, receipt.blockNumber, nowIso(), itemId);
    db.prepare(`INSERT OR IGNORE INTO distribution_receipts(id,item_id,chain_id,contract_address,recipient_address,amount_token_base,tx_hash,receipt_json,verified_at) VALUES(?,?,?,?,?,?,?,?,?)`)
      .run(uuid(), itemId, receipt.chainId, receipt.contract, receipt.recipient, receipt.amountBase, txHash, JSON.stringify(input.receipt || receipt), nowIso());
    const remaining = Number(db.prepare("SELECT COUNT(*) AS count FROM distribution_items WHERE campaign_id=? AND status NOT IN ('confirmed','cancelled')").get(before.campaign_id).count);
    if (!remaining) db.prepare("UPDATE distribution_campaigns SET status='completed',completed_at=?,updated_at=? WHERE id=?").run(nowIso(), nowIso(), before.campaign_id);
    const after = db.prepare('SELECT * FROM distribution_items WHERE id=?').get(itemId);
    audit(db, { actorType: 'admin', actorId: actor, action: 'distribution.item.confirm', targetType: 'distribution_item', targetId: itemId, before, after, ip });
    return { item: after, duplicate: false };
  });
}

export function refreshCampaignWallets(db, campaignId, actor = 'admin', ip = '') {
  return withTransaction(db, () => {
    const campaign = db.prepare('SELECT * FROM distribution_campaigns WHERE id=?').get(campaignId);
    if (!campaign) fail('DISTRIBUTION_CAMPAIGN_NOT_FOUND', 'Dağıtım kampanyası bulunamadı.', 404);
    const waiting = db.prepare("SELECT * FROM distribution_items WHERE campaign_id=? AND status='waiting_wallet'").all(campaignId);
    let updated = 0;
    for (const item of waiting) {
      const wallet = eligibleWallet(db, item.user_id);
      if (!wallet) continue;
      const nextStatus = campaign.mode === 'manual_withdrawal' ? 'manual' : 'ready';
      db.prepare("UPDATE distribution_items SET wallet_id=?,address=?,status=?,updated_at=? WHERE id=?").run(wallet.id, wallet.address, nextStatus, nowIso(), item.id);
      updated += 1;
    }
    audit(db, { actorType: 'admin', actorId: actor, action: 'distribution.wallets.refresh', targetType: 'distribution_campaign', targetId: campaignId, before: { waiting: waiting.length }, after: { updated }, ip });
    return { updated, campaign: db.prepare('SELECT * FROM distribution_campaigns WHERE id=?').get(campaignId) };
  });
}

export async function launchAutomationTick(db) {
  if (Date.now() < new Date(getSetting(db, 'launch_at', 0)).getTime()) return { skipped: 'before_launch' };
  const finalization = maybeFinalizeAtLaunch(db, 'launch-worker');
  if (!Boolean(getSetting(db, 'distribution_auto_start', false))) return { finalization, skipped: 'auto_distribution_disabled' };
  let campaign = db.prepare("SELECT * FROM distribution_campaigns WHERE status IN ('ready','running','paused','manual_open') ORDER BY created_at DESC LIMIT 1").get();
  if (!campaign) campaign = createDistributionCampaign(db, {}, 'launch-worker', '').campaign;
  if (campaign.mode === 'auto_contract') return { finalization, distribution: await runDistributionCampaign(db, campaign.id, 'launch-worker', '') };
  return { finalization, distribution: { campaign, manual: true } };
}
