import crypto from 'node:crypto';
import { config } from './config.js';
import { audit, getSetting, getSettings, setSetting, withTransaction } from './db.js';
import {
  ensureUserAccounts,
  getBalance,
  getUserBalances,
  getUserTransactions,
  postTransferInTx,
  reconcileLedger,
  userAccountId
} from './ledger.js';
import {
  clampInt,
  dayKey,
  hmac,
  nowIso,
  nowSeconds,
  periodKey,
  safeEqual,
  sanitizeText,
  sha256,
  uuid,
  weightedPick
} from './utils.js';

const BASE_PLAN = Object.freeze({
  tier: 0,
  name: 'Ücretsiz',
  subtitle: 'Başlangıç',
  tap_multiplier_bps: 10000,
  energy_multiplier_bps: 10000,
  auto_multiplier_bps: 10000,
  daily_free_spins: 0,
  referral_multiplier_bps: 10000,
  enabled: 1
});

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

function jsonValue(value) {
  try { return JSON.parse(value); } catch { return {}; }
}

function getVipPlan(db, tier) {
  if (!tier) return BASE_PLAN;
  return db.prepare('SELECT * FROM vip_plans WHERE tier=? AND enabled=1').get(tier) || BASE_PLAN;
}

function currentVipPlanInTx(db, state, now = nowSeconds()) {
  if (state.vip_tier > 0 && state.vip_expires_at && state.vip_expires_at <= now) {
    db.prepare('UPDATE user_game_state SET vip_tier=0,vip_expires_at=NULL,updated_at=? WHERE user_id=?')
      .run(nowIso(), state.user_id);
    state.vip_tier = 0;
    state.vip_expires_at = null;
  }
  return getVipPlan(db, state.vip_tier);
}

function getActiveTapMultiplierBps(db, userId, now = nowSeconds()) {
  const rows = db.prepare(`
    SELECT multiplier_bps FROM user_boosters
    WHERE user_id=? AND type='tap_multiplier' AND starts_at<=? AND expires_at>?
    ORDER BY multiplier_bps DESC
  `).all(userId, now, now);
  return rows.length ? Math.max(...rows.map((row) => row.multiplier_bps)) : 10000;
}

function getStateForUpdate(db, userId) {
  const state = db.prepare('SELECT * FROM user_game_state WHERE user_id=?').get(userId);
  if (!state) fail('GAME_STATE_NOT_FOUND', 'Oyun durumu bulunamadı.', 404);
  return state;
}

function settleEnergyInTx(db, state, plan, now = nowSeconds()) {
  const baseCapacity = Number(getSetting(db, 'energy_capacity', 1000));
  const capacity = Math.max(1, Math.floor(baseCapacity * plan.energy_multiplier_bps / 10000));
  const refillPerMinute = Math.max(0, Number(getSetting(db, 'energy_refill_per_minute', 10)));
  const elapsed = Math.max(0, now - state.energy_updated_at);
  if (state.energy >= capacity) {
    state.energy = capacity;
    state.energy_updated_at = now;
  } else if (elapsed > 0 && refillPerMinute > 0) {
    const refill = Math.floor(elapsed * refillPerMinute / 60);
    if (refill > 0) {
      const before = state.energy;
      state.energy = Math.min(capacity, state.energy + refill);
      if (state.energy >= capacity) {
        state.energy_updated_at = now;
      } else {
        const actual = state.energy - before;
        state.energy_updated_at += Math.max(1, Math.floor(actual * 60 / refillPerMinute));
      }
    }
  }
  db.prepare('UPDATE user_game_state SET energy=?,energy_updated_at=?,updated_at=? WHERE user_id=?')
    .run(state.energy, state.energy_updated_at, nowIso(), state.user_id);
  return capacity;
}

function settleAutomaticEarningsInTx(db, state, plan, now = nowSeconds()) {
  const perHour = Math.max(0, Number(getSetting(db, 'auto_points_per_hour', 125)));
  const capHours = Math.max(1, Number(getSetting(db, 'auto_offline_cap_hours', 8)));
  const elapsed = Math.max(0, Math.min(now - state.auto_settled_at, capHours * 3600));
  const effectiveRate = Math.floor(perHour * plan.auto_multiplier_bps / 10000);
  const reward = Math.floor(elapsed * effectiveRate / 3600);
  if (reward <= 0) return 0;
  const consumedSeconds = Math.max(1, Math.floor(reward * 3600 / Math.max(1, effectiveRate)));
  const nextSettledAt = Math.min(now, state.auto_settled_at + consumedSeconds);
  postTransferInTx(db, {
    fromAccountId: 'sys-points-reward',
    toAccountId: userAccountId(state.user_id, 'OC_POINTS'),
    amount: reward,
    type: 'automatic_earning',
    ref: state.user_id,
    idempotencyKey: `auto:${state.user_id}:${state.auto_settled_at}:${nextSettledAt}`,
    metadata: { elapsedSeconds: consumedSeconds, vipTier: plan.tier }
  });
  state.auto_settled_at = nextSettledAt;
  db.prepare('UPDATE user_game_state SET auto_settled_at=?,updated_at=? WHERE user_id=?')
    .run(nextSettledAt, nowIso(), state.user_id);
  return reward;
}

function updateLoginStreakInTx(db, state) {
  const today = dayKey();
  if (state.last_login_day === today) return;
  const yesterday = dayKey(new Date(Date.now() - 86400000));
  state.streak_days = state.last_login_day === yesterday ? state.streak_days + 1 : 1;
  state.last_login_day = today;
  db.prepare('UPDATE user_game_state SET last_login_day=?,streak_days=?,updated_at=? WHERE user_id=?')
    .run(today, state.streak_days, nowIso(), state.user_id);
}

function settleUserInTx(db, userId) {
  const user = db.prepare('SELECT * FROM users WHERE id=?').get(userId);
  if (!user || user.status === 'deleted') fail('USER_NOT_FOUND', 'Kullanıcı bulunamadı.', 404);
  if (user.status === 'blocked') fail('ACCOUNT_BLOCKED', 'Hesap erişimi geçici olarak durduruldu.', 403);
  ensureUserAccounts(db, userId);
  const state = getStateForUpdate(db, userId);
  const plan = currentVipPlanInTx(db, state);
  const capacity = settleEnergyInTx(db, state, plan);
  const autoReward = settleAutomaticEarningsInTx(db, state, plan);
  updateLoginStreakInTx(db, state);
  return { user, state, plan, capacity, autoReward };
}

function recordEventInTx(db, userId, type, value, idempotencyKey, metadata = {}) {
  const existing = db.prepare('SELECT id FROM game_events WHERE idempotency_key=?').get(idempotencyKey);
  if (existing) return false;
  db.prepare(`INSERT INTO game_events(id,user_id,type,value,metadata,idempotency_key,created_at) VALUES(?,?,?,?,?,?,?)`)
    .run(uuid(), userId, type, value, JSON.stringify(metadata), idempotencyKey, nowIso());
  const quests = db.prepare('SELECT * FROM quests WHERE enabled=1 AND event_type=?').all(type);
  for (const quest of quests) {
    const pKey = periodKey(quest.repeat_type);
    db.prepare(`
      INSERT INTO quest_progress(user_id,quest_id,period_key,progress,claimed,updated_at)
      VALUES(?,?,?,?,0,?)
      ON CONFLICT(user_id,quest_id,period_key)
      DO UPDATE SET progress=MIN((SELECT target_count FROM quests WHERE id=excluded.quest_id),quest_progress.progress+excluded.progress),updated_at=excluded.updated_at
    `).run(userId, quest.id, pKey, Math.max(0, value), nowIso());
  }
  return true;
}

function runIdempotent(db, scope, key, fn) {
  if (!key || key.length < 8 || key.length > 160) fail('IDEMPOTENCY_KEY_REQUIRED', 'Geçerli bir Idempotency-Key gönderilmelidir.', 400);
  return withTransaction(db, () => {
    const existing = db.prepare('SELECT response_json FROM idempotency_responses WHERE scope=? AND key=?').get(scope, key);
    if (existing) return { ...JSON.parse(existing.response_json), duplicate: true };
    const response = fn();
    db.prepare('INSERT INTO idempotency_responses(scope,key,response_json,created_at) VALUES(?,?,?,?)')
      .run(scope, key, JSON.stringify(response), nowIso());
    return response;
  });
}

function nextReferralCode(db, preferred = '') {
  const base = sanitizeText(preferred, 12).replace(/[^A-Za-z0-9]/g, '').toUpperCase();
  for (let index = 0; index < 20; index += 1) {
    const suffix = crypto.randomBytes(3).toString('hex').toUpperCase();
    const candidate = `${base.slice(0, 6) || 'ONLY'}${suffix}`.slice(0, 12);
    if (!db.prepare('SELECT 1 FROM users WHERE referral_code=?').get(candidate)) return candidate;
  }
  return uuid().replaceAll('-', '').slice(0, 12).toUpperCase();
}

function createUserInTx(db, { externalId, username, displayName, avatar = '', referralCode = '' }) {
  const id = uuid();
  const createdAt = nowIso();
  const inviter = referralCode ? db.prepare('SELECT id FROM users WHERE referral_code=? AND status=?').get(referralCode.toUpperCase(), 'active') : null;
  const ownCode = nextReferralCode(db, username);
  db.prepare(`INSERT INTO users(id,telegram_id,username,display_name,avatar,referral_code,referred_by,status,created_at,updated_at)
              VALUES(?,?,?,?,?,?,?,'active',?,?)`)
    .run(id, externalId, sanitizeText(username, 40) || `user_${id.slice(0, 6)}`, sanitizeText(displayName, 60) || 'OnlyCoin Oyuncusu', sanitizeText(avatar, 500), ownCode, inviter?.id || null, createdAt, createdAt);
  const energy = Number(getSetting(db, 'energy_capacity', 1000));
  db.prepare(`INSERT INTO user_game_state(user_id,energy,energy_updated_at,auto_settled_at,created_at,updated_at)
              VALUES(?,?,?,?,?,?)`)
    .run(id, energy, nowSeconds(), nowSeconds(), createdAt, createdAt);
  ensureUserAccounts(db, id);
  const initialPoints = Math.max(0, Number(getSetting(db, 'initial_points', 5000)));
  if (initialPoints > 0) {
    postTransferInTx(db, {
      fromAccountId: 'sys-points-admin',
      toAccountId: userAccountId(id, 'OC_POINTS'),
      amount: initialPoints,
      type: 'welcome_reward',
      ref: id,
      idempotencyKey: `welcome:${id}`,
      metadata: { source: 'registration' }
    });
  }
  if (inviter) {
    db.prepare(`INSERT INTO referrals(id,inviter_id,invitee_id,status,created_at) VALUES(?,?,?,'pending',?)`)
      .run(uuid(), inviter.id, id, createdAt);
  }
  return db.prepare('SELECT * FROM users WHERE id=?').get(id);
}

export function getOrCreateUser(db, input) {
  const externalId = sanitizeText(input.externalId, 80);
  if (!externalId) fail('EXTERNAL_ID_REQUIRED', 'Kullanıcı kimliği gereklidir.', 400);
  return withTransaction(db, () => {
    let user = db.prepare('SELECT * FROM users WHERE telegram_id=?').get(externalId);
    if (!user) user = createUserInTx(db, input);
    else {
      db.prepare('UPDATE users SET username=?,display_name=?,avatar=?,updated_at=? WHERE id=?')
        .run(sanitizeText(input.username, 40) || user.username, sanitizeText(input.displayName, 60) || user.display_name,
          sanitizeText(input.avatar, 500) || user.avatar, nowIso(), user.id);
      user = db.prepare('SELECT * FROM users WHERE id=?').get(user.id);
    }
    settleUserInTx(db, user.id);
    return user;
  });
}

function applyReferralRewardsInTx(db, inviteeId) {
  const relation = db.prepare(`
    SELECT r.*, u.referred_by AS inviter_parent
    FROM referrals r
    JOIN users u ON u.id=r.inviter_id
    WHERE r.invitee_id=? AND r.status='pending'
  `).get(inviteeId);
  if (!relation) return [];
  const threshold = Number(getSetting(db, 'referral_qualifying_taps', 100));
  const inviteeState = getStateForUpdate(db, inviteeId);
  if (inviteeState.tap_total < threshold) return [];

  const rewards = [];
  const base = Number(getSetting(db, 'referral_reward_points', 1000));
  const directState = getStateForUpdate(db, relation.inviter_id);
  const directPlan = currentVipPlanInTx(db, directState);
  const directAmount = Math.max(1, Math.floor(base * directPlan.referral_multiplier_bps / 10000));
  postTransferInTx(db, {
    fromAccountId: 'sys-points-referral',
    toAccountId: userAccountId(relation.inviter_id, 'OC_POINTS'),
    amount: directAmount,
    type: 'referral_reward',
    ref: relation.id,
    idempotencyKey: `referral:${relation.id}:level:1`,
    metadata: { level: 1, inviteeId }
  });
  recordEventInTx(db, relation.inviter_id, 'referral', 1, `event:referral:${relation.id}:1`, { inviteeId });
  rewards.push({ userId: relation.inviter_id, level: 1, amount: directAmount });

  if (getSetting(db, 'referral_multilevel_enabled', false)) {
    let parentId = relation.inviter_parent;
    for (const level of [2, 3]) {
      if (!parentId) break;
      const bps = Number(getSetting(db, `referral_level_${level}_bps`, level === 2 ? 500 : 200));
      const amount = Math.max(1, Math.floor(base * bps / 10000));
      postTransferInTx(db, {
        fromAccountId: 'sys-points-referral',
        toAccountId: userAccountId(parentId, 'OC_POINTS'),
        amount,
        type: 'referral_reward',
        ref: relation.id,
        idempotencyKey: `referral:${relation.id}:level:${level}`,
        metadata: { level, inviteeId }
      });
      rewards.push({ userId: parentId, level, amount });
      parentId = db.prepare('SELECT referred_by FROM users WHERE id=?').get(parentId)?.referred_by || null;
    }
  }

  db.prepare(`UPDATE referrals SET status='rewarded',qualified_at=?,rewarded_at=? WHERE id=?`)
    .run(nowIso(), nowIso(), relation.id);
  return rewards;
}

export function getPublicConfig(db) {
  const settings = getSettings(db);
  const vipPlans = db.prepare('SELECT * FROM vip_plans WHERE enabled=1 ORDER BY tier').all();
  const wheelSegments = db.prepare('SELECT id,label,reward_type,reward_value,weight,sort_order,color FROM wheel_segments WHERE enabled=1 ORDER BY sort_order').all();
  return {
    appName: settings.app_name,
    launchAt: settings.launch_at,
    launchMessage: settings.launch_message,
    announcement: settings.announcement,
    maintenanceMode: Boolean(settings.maintenance_mode),
    conversion: {
      enabled: Boolean(settings.conversion_enabled),
      unlockAtLaunch: Boolean(settings.conversion_unlock_at_launch),
      ratePercent: Number(settings.conversion_rate_bps) / 100,
      minPoints: Number(settings.conversion_min_points),
      dailyLimitPoints: Number(settings.conversion_daily_limit_points)
    },
    game: {
      tapReward: Number(settings.tap_reward_points),
      energyCapacity: Number(settings.energy_capacity),
      energyRefillPerMinute: Number(settings.energy_refill_per_minute),
      autoPointsPerHour: Number(settings.auto_points_per_hour),
      paidSpinCost: Number(settings.paid_spin_cost_points),
      wheelDailyFreeSpins: Number(settings.wheel_daily_free_spins || 3),
      wheelDailyTotalLimit: Number(settings.wheel_daily_total_limit || 10),
      wheelSpinCooldownSeconds: Number(settings.wheel_spin_cooldown_seconds || 5),
      wheelAnimationMs: Number(settings.wheel_animation_ms || 4600),
      adSpinEnabled: Boolean(settings.ad_spin_enabled),
      adRewardPoints: Number(settings.ad_reward_points),
      referralReward: Number(settings.referral_reward_points)
    },
    channels: {
      telegram: Boolean(settings.telegram_enabled),
      pwa: Boolean(settings.pwa_enabled),
      android: Boolean(settings.android_enabled),
      ios: Boolean(settings.ios_enabled)
    },
    supportEmail: settings.support_email,
    vipPlans,
    wheelSegments,
    serverTime: new Date().toISOString()
  };
}

function wheelStatusForState(settings, state, plan, now = nowSeconds()) {
  const today = dayKey();
  const freeUsed = state.last_free_spin_day === today ? Number(state.free_spins_used || 0) : 0;
  const totalUsed = state.last_wheel_spin_day === today ? Number(state.wheel_spins_used || 0) : 0;
  const baseFreeSpins = Math.max(0, Number(settings.wheel_daily_free_spins || 3));
  // VIP plans store additional free spins. The base entitlement remains
  // globally manageable and VIP tiers add their configured bonus on top.
  const vipBonusSpins = plan.tier > 0 ? Math.max(0, Number(plan.daily_free_spins || 0)) : 0;
  const freeAllowed = baseFreeSpins + vipBonusSpins;
  const totalLimit = Math.max(freeAllowed, Number(settings.wheel_daily_total_limit || 10));
  const animationMs = Math.max(1800, Math.min(9000, Number(settings.wheel_animation_ms || 4600)));
  // The server lock is never shorter than the visible animation. This closes
  // the rapid-click/API replay window even if an unsafe cooldown is entered.
  const cooldownSeconds = Math.max(1, Math.ceil(animationMs / 1000), Number(settings.wheel_spin_cooldown_seconds || 5));
  const cooldownRemaining = Math.max(0, Number(state.last_wheel_spin_at || 0) + cooldownSeconds - now);
  return {
    freeSpinsAllowed: freeAllowed,
    freeSpinsUsed: freeUsed,
    freeSpinsRemaining: Math.max(0, freeAllowed - freeUsed),
    totalSpinsLimit: totalLimit,
    totalSpinsUsed: totalUsed,
    totalSpinsRemaining: Math.max(0, totalLimit - totalUsed),
    adSpinCredits: Number(state.ad_spin_credits || 0),
    cooldownSeconds,
    cooldownRemaining,
    animationMs,
    lastSegmentId: state.last_wheel_segment_id || ''
  };
}

function stateResponse(db, userId, settled = null) {
  const result = settled || settleUserInTx(db, userId);
  const state = getStateForUpdate(db, userId);
  const plan = currentVipPlanInTx(db, state);
  const capacity = Math.max(1, Math.floor(Number(getSetting(db, 'energy_capacity', 1000)) * plan.energy_multiplier_bps / 10000));
  const balances = getUserBalances(db, userId);
  const booster = db.prepare(`SELECT * FROM user_boosters WHERE user_id=? AND expires_at>? ORDER BY expires_at DESC LIMIT 1`).get(userId, nowSeconds());
  const settings = getSettings(db);
  return {
    user: {
      id: result.user.id,
      username: result.user.username,
      displayName: result.user.display_name,
      avatar: result.user.avatar,
      referralCode: result.user.referral_code,
      status: result.user.status
    },
    balances,
    state: {
      level: state.level,
      xp: state.xp,
      energy: state.energy,
      energyCapacity: capacity,
      tapTotal: state.tap_total,
      streakDays: state.streak_days,
      vipTier: state.vip_tier,
      vipExpiresAt: state.vip_expires_at ? new Date(state.vip_expires_at * 1000).toISOString() : null,
      autoPointsPerHour: Math.floor(Number(settings.auto_points_per_hour) * plan.auto_multiplier_bps / 10000),
      tapReward: Math.max(1, Math.floor(Number(settings.tap_reward_points) * plan.tap_multiplier_bps / 10000 * getActiveTapMultiplierBps(db, userId) / 10000)),
      adSpinCredits: state.ad_spin_credits,
      wheel: wheelStatusForState(settings, state, plan),
      activeBooster: booster ? {
        type: booster.type,
        multiplier: booster.multiplier_bps / 10000,
        expiresAt: new Date(booster.expires_at * 1000).toISOString()
      } : null
    },
    vipPlan: plan,
    launchAt: settings.launch_at,
    conversionEnabled: Boolean(settings.conversion_enabled),
    conversionRatePercent: Number(settings.conversion_rate_bps) / 100,
    serverTime: new Date().toISOString()
  };
}

export function getGameState(db, userId) {
  return withTransaction(db, () => stateResponse(db, userId, settleUserInTx(db, userId)));
}

export function tap(db, userId, countInput, idempotencyKey) {
  return runIdempotent(db, `tap:${userId}`, idempotencyKey, () => {
    const { user, state, plan } = settleUserInTx(db, userId);
    const maxBatch = Number(getSetting(db, 'tap_batch_limit', 50));
    const count = clampInt(countInput, 1, maxBatch, 0);
    if (!count) fail('INVALID_TAP_COUNT', `Dokunuş sayısı 1-${maxBatch} arasında olmalıdır.`, 400);
    if (state.energy <= 0) fail('ENERGY_EMPTY', 'Enerji tükendi. Yenilenmesini bekleyin veya enerji ödülü kullanın.', 409);
    const accepted = Math.min(count, state.energy);
    const baseReward = Number(getSetting(db, 'tap_reward_points', 5));
    const boosterBps = getActiveTapMultiplierBps(db, userId);
    const reward = Math.max(1, Math.floor(accepted * baseReward * plan.tap_multiplier_bps * boosterBps / 100000000));
    state.energy -= accepted;
    state.tap_total += accepted;
    state.xp += accepted;
    state.level = Math.max(1, Math.floor(Math.sqrt(state.xp / 100)) + 1);
    db.prepare(`UPDATE user_game_state SET energy=?,tap_total=?,xp=?,level=?,updated_at=? WHERE user_id=?`)
      .run(state.energy, state.tap_total, state.xp, state.level, nowIso(), userId);
    const tx = postTransferInTx(db, {
      fromAccountId: 'sys-points-reward',
      toAccountId: userAccountId(userId, 'OC_POINTS'),
      amount: reward,
      type: 'tap_reward',
      ref: idempotencyKey,
      idempotencyKey: `ledger:tap:${userId}:${idempotencyKey}`,
      metadata: { count: accepted, vipTier: plan.tier, boosterBps }
    });
    recordEventInTx(db, userId, 'tap', accepted, `event:tap:${userId}:${idempotencyKey}`, { reward });
    const referralRewards = applyReferralRewardsInTx(db, userId);
    return {
      acceptedTaps: accepted,
      rewardPoints: reward,
      transactionId: tx.id,
      referralRewards,
      ...stateResponse(db, userId, { user, state, plan })
    };
  });
}

function spinRewardInTx(db, userId, segment, spinId) {
  const state = getStateForUpdate(db, userId);
  if (segment.reward_type === 'points' && segment.reward_value > 0) {
    postTransferInTx(db, {
      fromAccountId: 'sys-points-reward',
      toAccountId: userAccountId(userId, 'OC_POINTS'),
      amount: segment.reward_value,
      type: 'wheel_reward',
      ref: spinId,
      idempotencyKey: `ledger:wheel:${spinId}`,
      metadata: { segmentId: segment.id }
    });
  } else if (segment.reward_type === 'energy') {
    const plan = currentVipPlanInTx(db, state);
    const capacity = Math.floor(Number(getSetting(db, 'energy_capacity', 1000)) * plan.energy_multiplier_bps / 10000);
    state.energy = Math.min(capacity, state.energy + segment.reward_value);
    db.prepare('UPDATE user_game_state SET energy=?,energy_updated_at=?,updated_at=? WHERE user_id=?')
      .run(state.energy, nowSeconds(), nowIso(), userId);
  } else if (segment.reward_type === 'booster') {
    db.prepare(`INSERT INTO user_boosters(id,user_id,type,multiplier_bps,starts_at,expires_at,source,created_at) VALUES(?,?,?,?,?,?,?,?)`)
      .run(uuid(), userId, 'tap_multiplier', segment.reward_value || 20000, nowSeconds(), nowSeconds() + 900, 'wheel', nowIso());
  } else if (segment.reward_type === 'vip_trial') {
    if (state.vip_tier < 1 || (state.vip_expires_at && state.vip_expires_at < nowSeconds() + segment.reward_value)) {
      state.vip_tier = Math.max(1, state.vip_tier);
      state.vip_expires_at = Math.max(state.vip_expires_at || 0, nowSeconds()) + segment.reward_value;
      db.prepare('UPDATE user_game_state SET vip_tier=?,vip_expires_at=?,updated_at=? WHERE user_id=?')
        .run(state.vip_tier, state.vip_expires_at, nowIso(), userId);
    }
  } else if (segment.reward_type === 'ad_spin') {
    state.ad_spin_credits += segment.reward_value;
    db.prepare('UPDATE user_game_state SET ad_spin_credits=?,updated_at=? WHERE user_id=?')
      .run(state.ad_spin_credits, nowIso(), userId);
  }
}

export function spinWheel(db, userId, mode, idempotencyKey) {
  return runIdempotent(db, `wheel:${userId}`, idempotencyKey, () => {
    const { user, state, plan } = settleUserInTx(db, userId);
    const settings = getSettings(db);
    const now = nowSeconds();
    const today = dayKey();

    if (state.last_free_spin_day !== today) {
      state.last_free_spin_day = today;
      state.free_spins_used = 0;
    }
    if (state.last_wheel_spin_day !== today) {
      state.last_wheel_spin_day = today;
      state.wheel_spins_used = 0;
    }

    const status = wheelStatusForState(settings, state, plan, now);
    if (status.totalSpinsRemaining <= 0) {
      fail('DAILY_WHEEL_LIMIT', 'Bugünkü toplam çark çevirme limitiniz doldu.', 409, status);
    }
    if (status.cooldownRemaining > 0) {
      fail('SPIN_IN_PROGRESS', 'Çark sonucu tamamlanmadan yeniden çevrilemez.', 429, { retryAfter: status.cooldownRemaining });
    }

    if (mode === 'free') {
      if (status.freeSpinsRemaining <= 0) fail('FREE_SPIN_USED', 'Bugünkü ücretsiz çark hakkınız tükendi.', 409, status);
      state.free_spins_used += 1;
    } else if (mode === 'points') {
      const cost = Number(getSetting(db, 'paid_spin_cost_points', 50));
      postTransferInTx(db, {
        fromAccountId: userAccountId(userId, 'OC_POINTS'),
        toAccountId: 'sys-points-revenue',
        amount: cost,
        type: 'wheel_purchase',
        ref: idempotencyKey,
        idempotencyKey: `ledger:wheel-cost:${userId}:${idempotencyKey}`,
        metadata: { mode }
      });
    } else if (mode === 'ad') {
      if (state.ad_spin_credits <= 0) fail('AD_SPIN_REQUIRED', 'Önce ödüllü reklam izleyerek çark hakkı kazanın.', 409);
      state.ad_spin_credits -= 1;
    } else {
      fail('INVALID_SPIN_MODE', 'Geçersiz çark modu.', 400);
    }

    const allSegments = db.prepare('SELECT * FROM wheel_segments WHERE enabled=1 AND weight>0 ORDER BY sort_order,id').all();
    if (!allSegments.length) fail('WHEEL_NOT_CONFIGURED', 'Aktif çark dilimi bulunamadı.', 409);
    let candidates = allSegments;
    if (Boolean(settings.wheel_prevent_immediate_repeat) && state.last_wheel_segment_id && allSegments.length > 1) {
      const filtered = allSegments.filter((item) => item.id !== state.last_wheel_segment_id);
      if (filtered.length) candidates = filtered;
    }
    const segment = weightedPick(candidates);
    const segmentIndex = allSegments.findIndex((item) => item.id === segment.id);
    const spinId = uuid();

    state.wheel_spins_used += 1;
    state.last_wheel_spin_at = now;
    state.last_wheel_segment_id = segment.id;
    db.prepare(`UPDATE user_game_state SET
      last_free_spin_day=?,free_spins_used=?,ad_spin_credits=?,last_wheel_spin_day=?,wheel_spins_used=?,last_wheel_spin_at=?,last_wheel_segment_id=?,updated_at=?
      WHERE user_id=?`)
      .run(state.last_free_spin_day, state.free_spins_used, state.ad_spin_credits, state.last_wheel_spin_day, state.wheel_spins_used, state.last_wheel_spin_at, state.last_wheel_segment_id, nowIso(), userId);

    spinRewardInTx(db, userId, segment, spinId);
    db.prepare(`INSERT INTO wheel_spins(id,user_id,mode,segment_id,reward_type,reward_value,idempotency_key,created_at) VALUES(?,?,?,?,?,?,?,?)`)
      .run(spinId, userId, mode, segment.id, segment.reward_type, segment.reward_value, idempotencyKey, nowIso());
    recordEventInTx(db, userId, 'spin', 1, `event:spin:${spinId}`, { segmentId: segment.id, mode });
    return {
      spinId,
      mode,
      segmentIndex,
      segmentCount: allSegments.length,
      segment: {
        id: segment.id,
        label: segment.label,
        rewardType: segment.reward_type,
        rewardValue: segment.reward_value,
        color: segment.color
      },
      ...stateResponse(db, userId, { user, state, plan })
    };
  });
}

export function startAdSession(db, userId) {
  return withTransaction(db, () => {
    settleUserInTx(db, userId);
    if (!getSetting(db, 'ad_spin_enabled', true)) fail('ADS_DISABLED', 'Ödüllü reklam şu anda kapalı.', 409);
    const cooldown = Number(getSetting(db, 'ad_cooldown_seconds', 60));
    const latest = db.prepare(`SELECT completed_at FROM ad_sessions WHERE user_id=? AND status='completed' ORDER BY completed_at DESC LIMIT 1`).get(userId);
    if (latest?.completed_at && latest.completed_at + cooldown > nowSeconds()) {
      fail('AD_COOLDOWN', 'Yeni reklam için kısa bir süre beklemelisiniz.', 429, { retryAfter: latest.completed_at + cooldown - nowSeconds() });
    }
    const id = uuid();
    const nonce = crypto.randomBytes(24).toString('base64url');
    const token = `${id}.${nonce}.${hmac(config.authSecret, `${id}.${nonce}.${userId}`)}`;
    db.prepare(`INSERT INTO ad_sessions(id,user_id,token_hash,status,started_at,created_at) VALUES(?,?,?,'started',?,?)`)
      .run(id, userId, sha256(token), nowSeconds(), nowIso());
    return { sessionId: id, adToken: token, minimumWatchSeconds: 15, providerMode: 'server-verification-required' };
  });
}

export function completeAdSession(db, userId, token) {
  return withTransaction(db, () => {
    const parts = String(token || '').split('.');
    if (parts.length !== 3) fail('INVALID_AD_TOKEN', 'Reklam oturumu geçersiz.', 401);
    const [id, nonce, signature] = parts;
    if (!safeEqual(signature, hmac(config.authSecret, `${id}.${nonce}.${userId}`))) fail('INVALID_AD_TOKEN', 'Reklam oturumu doğrulanamadı.', 401);
    const session = db.prepare('SELECT * FROM ad_sessions WHERE id=? AND user_id=? AND token_hash=?').get(id, userId, sha256(token));
    if (!session) fail('AD_SESSION_NOT_FOUND', 'Reklam oturumu bulunamadı.', 404);
    if (session.status === 'completed') return { completed: true, duplicate: true, ...stateResponse(db, userId) };
    const minimum = 15;
    if (session.started_at + minimum > nowSeconds()) fail('AD_NOT_COMPLETED', 'Reklam henüz tamamlanmadı.', 409, { retryAfter: session.started_at + minimum - nowSeconds() });
    const { user, state, plan } = settleUserInTx(db, userId);
    state.ad_spin_credits += 1;
    db.prepare(`UPDATE ad_sessions SET status='completed',completed_at=? WHERE id=?`).run(nowSeconds(), id);
    db.prepare(`UPDATE user_game_state SET ad_spin_credits=?,updated_at=? WHERE user_id=?`).run(state.ad_spin_credits, nowIso(), userId);
    const reward = Number(getSetting(db, 'ad_reward_points', 250));
    if (reward > 0) {
      postTransferInTx(db, {
        fromAccountId: 'sys-points-reward',
        toAccountId: userAccountId(userId, 'OC_POINTS'),
        amount: reward,
        type: 'ad_reward',
        ref: id,
        idempotencyKey: `ledger:ad:${id}`,
        metadata: { verifiedBy: 'server-session' }
      });
    }
    recordEventInTx(db, userId, 'ad', 1, `event:ad:${id}`, { reward });
    return { completed: true, rewardPoints: reward, adSpinCreditsAdded: 1, ...stateResponse(db, userId, { user, state, plan }) };
  });
}

export function listQuests(db, userId) {
  return withTransaction(db, () => {
    settleUserInTx(db, userId);
    const quests = db.prepare('SELECT * FROM quests WHERE enabled=1 ORDER BY sort_order,id').all();
    return quests.map((quest) => {
      const pKey = periodKey(quest.repeat_type);
      const progress = db.prepare('SELECT progress,claimed FROM quest_progress WHERE user_id=? AND quest_id=? AND period_key=?').get(userId, quest.id, pKey) || { progress: 0, claimed: 0 };
      return {
        ...quest,
        periodKey: pKey,
        progress: progress.progress,
        claimed: Boolean(progress.claimed),
        completed: progress.progress >= quest.target_count
      };
    });
  });
}

export function claimQuest(db, userId, questId, idempotencyKey) {
  return runIdempotent(db, `quest:${userId}:${questId}`, idempotencyKey, () => {
    const { user, state, plan } = settleUserInTx(db, userId);
    const quest = db.prepare('SELECT * FROM quests WHERE id=? AND enabled=1').get(questId);
    if (!quest) fail('QUEST_NOT_FOUND', 'Görev bulunamadı.', 404);
    const pKey = periodKey(quest.repeat_type);
    const progress = db.prepare('SELECT * FROM quest_progress WHERE user_id=? AND quest_id=? AND period_key=?').get(userId, quest.id, pKey);
    if (!progress || progress.progress < quest.target_count) fail('QUEST_NOT_COMPLETED', 'Görev henüz tamamlanmadı.', 409);
    if (progress.claimed) fail('QUEST_ALREADY_CLAIMED', 'Bu görev ödülü daha önce alındı.', 409);
    db.prepare('UPDATE quest_progress SET claimed=1,updated_at=? WHERE user_id=? AND quest_id=? AND period_key=?')
      .run(nowIso(), userId, quest.id, pKey);
    postTransferInTx(db, {
      fromAccountId: 'sys-points-quest',
      toAccountId: userAccountId(userId, 'OC_POINTS'),
      amount: quest.reward_points,
      type: 'quest_reward',
      ref: `${quest.id}:${pKey}`,
      idempotencyKey: `ledger:quest:${userId}:${quest.id}:${pKey}`,
      metadata: { questId: quest.id, periodKey: pKey }
    });
    return { questId: quest.id, rewardPoints: quest.reward_points, ...stateResponse(db, userId, { user, state, plan }) };
  });
}

export function convertPoints(db, userId, pointsInput, idempotencyKey) {
  return runIdempotent(db, `convert:${userId}`, idempotencyKey, () => {
    const { user, state, plan } = settleUserInTx(db, userId);
    if (!getSetting(db, 'conversion_enabled', false)) fail('CONVERSION_DISABLED', 'OC_POINTS dönüşümü şu anda kapalı.', 409);
    const launchAt = new Date(getSetting(db, 'launch_at')).getTime();
    if (getSetting(db, 'conversion_unlock_at_launch', true) && Date.now() < launchAt) {
      fail('CONVERSION_LOCKED_UNTIL_LAUNCH', 'Dönüşüm lansman tarihinde açılacak.', 409, { launchAt: new Date(launchAt).toISOString() });
    }
    const min = Number(getSetting(db, 'conversion_min_points', 1000));
    const points = clampInt(pointsInput, min, 2_000_000_000, 0);
    if (!points) fail('INVALID_CONVERSION_AMOUNT', `Minimum dönüşüm miktarı ${min} OC_POINTS.`, 400);
    const dailyLimit = Number(getSetting(db, 'conversion_daily_limit_points', 1000000));
    const today = dayKey();
    const used = db.prepare(`SELECT COALESCE(SUM(points_amount),0) AS total FROM conversion_records WHERE user_id=? AND substr(created_at,1,10)=?`).get(userId, today).total;
    if (used + points > dailyLimit) fail('CONVERSION_DAILY_LIMIT', 'Günlük dönüşüm limiti aşılıyor.', 409, { used, dailyLimit });
    const rateBps = Number(getSetting(db, 'conversion_rate_bps', 1000));
    const tokens = Math.floor(points * rateBps / 10000);
    if (tokens <= 0) fail('CONVERSION_RESULT_ZERO', 'Bu miktar dönüşüm için çok düşük.', 400);
    const conversionId = uuid();
    postTransferInTx(db, {
      fromAccountId: userAccountId(userId, 'OC_POINTS'),
      toAccountId: 'sys-points-sink',
      amount: points,
      type: 'points_conversion_burn',
      ref: conversionId,
      idempotencyKey: `ledger:convert:points:${userId}:${idempotencyKey}`,
      metadata: { rateBps, tokens }
    });
    postTransferInTx(db, {
      fromAccountId: 'sys-token-treasury',
      toAccountId: userAccountId(userId, 'OC_TOKEN'),
      amount: tokens,
      type: 'token_conversion_credit',
      ref: conversionId,
      idempotencyKey: `ledger:convert:token:${userId}:${idempotencyKey}`,
      metadata: { rateBps, points }
    });
    db.prepare(`INSERT INTO conversion_records(id,user_id,points_amount,token_amount,rate_bps,idempotency_key,created_at) VALUES(?,?,?,?,?,?,?)`)
      .run(conversionId, userId, points, tokens, rateBps, idempotencyKey, nowIso());
    recordEventInTx(db, userId, 'convert', 1, `event:convert:${conversionId}`, { points, tokens, rateBps });
    return { conversionId, pointsSpent: points, tokensReceived: tokens, ratePercent: rateBps / 100, ...stateResponse(db, userId, { user, state, plan }) };
  });
}

export function buyVipWithPoints(db, userId, tierInput, idempotencyKey) {
  return runIdempotent(db, `vip:${userId}`, idempotencyKey, () => {
    const { user, state } = settleUserInTx(db, userId);
    const tier = clampInt(tierInput, 1, 5, 0);
    const plan = db.prepare('SELECT * FROM vip_plans WHERE tier=? AND enabled=1').get(tier);
    if (!plan) fail('VIP_PLAN_NOT_FOUND', 'VIP paketi bulunamadı.', 404);
    if (tier <= state.vip_tier && (!state.vip_expires_at || state.vip_expires_at > nowSeconds())) fail('VIP_ALREADY_ACTIVE', 'Bu VIP seviyesi veya daha yükseği zaten aktif.', 409);
    postTransferInTx(db, {
      fromAccountId: userAccountId(userId, 'OC_POINTS'),
      toAccountId: 'sys-points-revenue',
      amount: plan.price_points,
      type: 'vip_purchase_points',
      ref: `vip:${tier}`,
      idempotencyKey: `ledger:vip:${userId}:${idempotencyKey}`,
      metadata: { tier }
    });
    state.vip_tier = tier;
    state.vip_expires_at = null;
    db.prepare('UPDATE user_game_state SET vip_tier=?,vip_expires_at=NULL,updated_at=? WHERE user_id=?')
      .run(tier, nowIso(), userId);
    return { purchasedTier: tier, pricePoints: plan.price_points, ...stateResponse(db, userId, { user, state, plan }) };
  });
}

export function referralSummary(db, userId) {
  const user = db.prepare('SELECT referral_code FROM users WHERE id=?').get(userId);
  const invited = db.prepare(`SELECT COUNT(*) AS count FROM referrals WHERE inviter_id=?`).get(userId).count;
  const qualified = db.prepare(`SELECT COUNT(*) AS count FROM referrals WHERE inviter_id=? AND status IN ('qualified','rewarded')`).get(userId).count;
  const total = db.prepare(`
    SELECT COALESCE(SUM(e.amount),0) AS total
    FROM ledger_entries e
    JOIN ledger_transactions t ON t.id=e.transaction_id
    JOIN ledger_accounts a ON a.id=e.account_id
    WHERE a.user_id=? AND t.type='referral_reward' AND e.amount>0
  `).get(userId).total;
  const recent = db.prepare(`
    SELECT u.display_name,u.username,r.status,r.created_at,r.rewarded_at
    FROM referrals r JOIN users u ON u.id=r.invitee_id
    WHERE r.inviter_id=? ORDER BY r.created_at DESC LIMIT 20
  `).all(userId);
  return {
    referralCode: user.referral_code,
    referralLink: `${config.appOrigin}/game/?ref=${encodeURIComponent(user.referral_code)}`,
    invited,
    qualified,
    totalEarnedPoints: total,
    levels: [
      { level: 1, percentage: Number(getSetting(db, 'referral_level_1_bps', 1000)) / 100, enabled: true },
      { level: 2, percentage: Number(getSetting(db, 'referral_level_2_bps', 500)) / 100, enabled: Boolean(getSetting(db, 'referral_multilevel_enabled', false)) },
      { level: 3, percentage: Number(getSetting(db, 'referral_level_3_bps', 200)) / 100, enabled: Boolean(getSetting(db, 'referral_multilevel_enabled', false)) }
    ],
    recent
  };
}

export function leaderboard(db, limit = 50) {
  const safeLimit = Math.max(3, Math.min(100, Number(limit) || 50));
  return db.prepare(`
    SELECT u.id,u.display_name,u.username,u.avatar,s.vip_tier,s.level,s.tap_total,a.balance AS points
    FROM users u
    JOIN user_game_state s ON s.user_id=u.id
    JOIN ledger_accounts a ON a.user_id=u.id AND a.asset='OC_POINTS'
    WHERE u.status='active'
    ORDER BY a.balance DESC,s.tap_total DESC,u.created_at ASC
    LIMIT ?
  `).all(safeLimit).map((row, index) => ({ rank: index + 1, ...row }));
}

export function wallet(db, userId) {
  return withTransaction(db, () => {
    const state = stateResponse(db, userId, settleUserInTx(db, userId));
    const conversions = db.prepare('SELECT * FROM conversion_records WHERE user_id=? ORDER BY created_at DESC LIMIT 20').all(userId);
    return { ...state, transactions: getUserTransactions(db, userId, 50), conversions };
  });
}

export function adminDashboard(db) {
  const users = db.prepare(`SELECT COUNT(*) AS count FROM users WHERE status<>'deleted'`).get().count;
  const activeToday = db.prepare(`SELECT COUNT(DISTINCT user_id) AS count FROM game_events WHERE substr(created_at,1,10)=?`).get(dayKey()).count;
  const tapsToday = db.prepare(`SELECT COALESCE(SUM(value),0) AS total FROM game_events WHERE type='tap' AND substr(created_at,1,10)=?`).get(dayKey()).total;
  const conversions = db.prepare(`SELECT COALESCE(SUM(points_amount),0) AS points,COALESCE(SUM(token_amount),0) AS tokens FROM conversion_records`).get();
  const vipDistribution = db.prepare(`SELECT vip_tier,COUNT(*) AS users FROM user_game_state GROUP BY vip_tier ORDER BY vip_tier`).all();
  const topUsers = leaderboard(db, 10);
  const ledger = reconcileLedger(db);
  const systemAccounts = db.prepare(`SELECT code,asset,balance FROM ledger_accounts WHERE user_id IS NULL ORDER BY asset,code`).all();
  return { users, activeToday, tapsToday, conversions, vipDistribution, topUsers, ledger, systemAccounts, settings: getSettings(db) };
}

const editableSettings = new Set([
  'app_name','launch_at','launch_message','announcement','maintenance_mode','conversion_enabled','conversion_unlock_at_launch',
  'conversion_rate_bps','conversion_min_points','conversion_daily_limit_points','initial_points','tap_reward_points','tap_batch_limit',
  'energy_capacity','energy_refill_per_minute','auto_points_per_hour','auto_offline_cap_hours','paid_spin_cost_points','wheel_daily_free_spins','wheel_daily_total_limit','wheel_spin_cooldown_seconds','wheel_animation_ms','wheel_prevent_immediate_repeat','ad_spin_enabled',
  'ad_reward_points','ad_cooldown_seconds','referral_reward_points','referral_qualifying_taps','referral_level_1_bps','referral_level_2_bps',
  'referral_level_3_bps','referral_multilevel_enabled','telegram_enabled','pwa_enabled','android_enabled','ios_enabled','purchases_enabled',
  'support_email','terms_url','privacy_url'
]);

export function adminUpdateSettings(db, changes, actor, ip) {
  return withTransaction(db, () => {
    const before = getSettings(db);
    for (const [key, value] of Object.entries(changes || {})) {
      if (!editableSettings.has(key)) continue;
      if (key === 'launch_at' && Number.isNaN(new Date(value).getTime())) fail('INVALID_LAUNCH_DATE', 'Lansman tarihi geçersiz.', 400);
      if (key === 'conversion_rate_bps' && (Number(value) < 1 || Number(value) > 100000)) fail('INVALID_CONVERSION_RATE', 'Dönüşüm oranı 0.01% ile 1000% arasında olmalıdır.', 400);
      setSetting(db, key, value);
    }
    const after = getSettings(db);
    audit(db, { actorType: 'admin', actorId: actor, action: 'settings.update', targetType: 'settings', targetId: 'global', before, after, ip });
    return after;
  });
}

export function adminListVip(db) {
  return db.prepare('SELECT * FROM vip_plans ORDER BY tier').all();
}

export function adminUpdateVip(db, tierInput, values, actor, ip) {
  const tier = clampInt(tierInput, 1, 5, 0);
  if (!tier) fail('INVALID_VIP_TIER', 'VIP seviyesi geçersiz.', 400);
  return withTransaction(db, () => {
    const before = db.prepare('SELECT * FROM vip_plans WHERE tier=?').get(tier);
    if (!before) fail('VIP_PLAN_NOT_FOUND', 'VIP planı bulunamadı.', 404);
    const next = {
      name: sanitizeText(values.name ?? before.name, 40),
      subtitle: sanitizeText(values.subtitle ?? before.subtitle, 60),
      price_points: clampInt(values.price_points ?? before.price_points, 0, 2_000_000_000, before.price_points),
      price_try_cents: clampInt(values.price_try_cents ?? before.price_try_cents, 0, 2_000_000_000, before.price_try_cents),
      tap_multiplier_bps: clampInt(values.tap_multiplier_bps ?? before.tap_multiplier_bps, 10000, 100000, before.tap_multiplier_bps),
      energy_multiplier_bps: clampInt(values.energy_multiplier_bps ?? before.energy_multiplier_bps, 10000, 100000, before.energy_multiplier_bps),
      auto_multiplier_bps: clampInt(values.auto_multiplier_bps ?? before.auto_multiplier_bps, 10000, 100000, before.auto_multiplier_bps),
      daily_free_spins: clampInt(values.daily_free_spins ?? before.daily_free_spins, 0, 100, before.daily_free_spins),
      referral_multiplier_bps: clampInt(values.referral_multiplier_bps ?? before.referral_multiplier_bps, 10000, 100000, before.referral_multiplier_bps),
      enabled: values.enabled === false || values.enabled === 0 ? 0 : 1
    };
    db.prepare(`UPDATE vip_plans SET name=?,subtitle=?,price_points=?,price_try_cents=?,tap_multiplier_bps=?,energy_multiplier_bps=?,auto_multiplier_bps=?,daily_free_spins=?,referral_multiplier_bps=?,enabled=?,updated_at=? WHERE tier=?`)
      .run(next.name,next.subtitle,next.price_points,next.price_try_cents,next.tap_multiplier_bps,next.energy_multiplier_bps,next.auto_multiplier_bps,next.daily_free_spins,next.referral_multiplier_bps,next.enabled,nowIso(),tier);
    const after = db.prepare('SELECT * FROM vip_plans WHERE tier=?').get(tier);
    audit(db, { actorType: 'admin', actorId: actor, action: 'vip.update', targetType: 'vip_plan', targetId: String(tier), before, after, ip });
    return after;
  });
}

export function adminListWheel(db) {
  return db.prepare('SELECT * FROM wheel_segments ORDER BY sort_order').all();
}

export function adminUpdateWheel(db, id, values, actor, ip) {
  return withTransaction(db, () => {
    const before = db.prepare('SELECT * FROM wheel_segments WHERE id=?').get(id);
    if (!before) fail('WHEEL_SEGMENT_NOT_FOUND', 'Çark dilimi bulunamadı.', 404);
    const rewardTypes = ['points','energy','booster','vip_trial','ad_spin'];
    const rewardType = rewardTypes.includes(values.reward_type) ? values.reward_type : before.reward_type;
    const next = {
      label: sanitizeText(values.label ?? before.label, 80),
      reward_type: rewardType,
      reward_value: clampInt(values.reward_value ?? before.reward_value, 0, 2_000_000_000, before.reward_value),
      weight: clampInt(values.weight ?? before.weight, 0, 1000000, before.weight),
      enabled: values.enabled === false || values.enabled === 0 ? 0 : 1,
      sort_order: clampInt(values.sort_order ?? before.sort_order, 0, 1000, before.sort_order)
    };
    const color = /^#[0-9a-f]{6}$/i.test(String(values.color || '')) ? values.color : before.color;
    db.prepare(`UPDATE wheel_segments SET label=?,reward_type=?,reward_value=?,weight=?,enabled=?,sort_order=?,color=?,updated_at=? WHERE id=?`)
      .run(next.label,next.reward_type,next.reward_value,next.weight,next.enabled,next.sort_order,color,nowIso(),id);
    const after = db.prepare('SELECT * FROM wheel_segments WHERE id=?').get(id);
    audit(db, { actorType: 'admin', actorId: actor, action: 'wheel.update', targetType: 'wheel_segment', targetId: id, before, after, ip });
    return after;
  });
}

export function adminListQuests(db) {
  return db.prepare('SELECT * FROM quests ORDER BY sort_order,id').all();
}

export function adminUpdateQuest(db, id, values, actor, ip) {
  return withTransaction(db, () => {
    const before = db.prepare('SELECT * FROM quests WHERE id=?').get(id);
    if (!before) fail('QUEST_NOT_FOUND', 'Görev bulunamadı.', 404);
    const repeatTypes = ['daily','weekly','once'];
    const next = {
      title: sanitizeText(values.title ?? before.title, 80),
      description: sanitizeText(values.description ?? before.description, 220),
      target_count: clampInt(values.target_count ?? before.target_count, 1, 100000000, before.target_count),
      reward_points: clampInt(values.reward_points ?? before.reward_points, 0, 2000000000, before.reward_points),
      repeat_type: repeatTypes.includes(values.repeat_type) ? values.repeat_type : before.repeat_type,
      sort_order: clampInt(values.sort_order ?? before.sort_order, 0, 1000, before.sort_order),
      enabled: values.enabled === false || values.enabled === 0 ? 0 : 1
    };
    db.prepare(`UPDATE quests SET title=?,description=?,target_count=?,reward_points=?,repeat_type=?,sort_order=?,enabled=?,updated_at=? WHERE id=?`)
      .run(next.title,next.description,next.target_count,next.reward_points,next.repeat_type,next.sort_order,next.enabled,nowIso(),id);
    const after = db.prepare('SELECT * FROM quests WHERE id=?').get(id);
    audit(db, { actorType: 'admin', actorId: actor, action: 'quest.update', targetType: 'quest', targetId: id, before, after, ip });
    return after;
  });
}

export function adminUsers(db, search = '', limit = 100) {
  const term = `%${sanitizeText(search, 60)}%`;
  const safeLimit = Math.max(1, Math.min(500, Number(limit) || 100));
  return db.prepare(`
    SELECT u.*,s.level,s.tap_total,s.vip_tier,s.streak_days,
      MAX(CASE WHEN a.asset='OC_POINTS' THEN a.balance ELSE 0 END) AS points,
      MAX(CASE WHEN a.asset='OC_TOKEN' THEN a.balance ELSE 0 END) AS tokens
    FROM users u
    JOIN user_game_state s ON s.user_id=u.id
    JOIN ledger_accounts a ON a.user_id=u.id
    WHERE u.username LIKE ? OR u.display_name LIKE ? OR u.referral_code LIKE ?
    GROUP BY u.id
    ORDER BY u.created_at DESC
    LIMIT ?
  `).all(term, term, term, safeLimit);
}

export function adminGrantPoints(db, userId, amountInput, reason, actor, ip, idempotencyKey) {
  const amount = clampInt(amountInput, 1, 2_000_000_000, 0);
  if (!amount) fail('INVALID_AMOUNT', 'Dağıtım miktarı geçersiz.', 400);
  return runIdempotent(db, `admin-grant:${userId}`, idempotencyKey, () => {
    const user = db.prepare('SELECT * FROM users WHERE id=?').get(userId);
    if (!user) fail('USER_NOT_FOUND', 'Kullanıcı bulunamadı.', 404);
    const tx = postTransferInTx(db, {
      fromAccountId: 'sys-points-admin',
      toAccountId: userAccountId(userId, 'OC_POINTS'),
      amount,
      type: 'admin_grant',
      ref: reason || 'admin',
      idempotencyKey: `ledger:admin-grant:${userId}:${idempotencyKey}`,
      metadata: { reason: sanitizeText(reason, 200), actor }
    });
    audit(db, { actorType: 'admin', actorId: actor, action: 'points.grant', targetType: 'user', targetId: userId, after: { amount, transactionId: tx.id, reason }, ip });
    return { userId, amount, balance: getBalance(db, userAccountId(userId, 'OC_POINTS')), transactionId: tx.id };
  });
}

export function adminSetVip(db, userId, tierInput, actor, ip) {
  const tier = clampInt(tierInput, 0, 5, -1);
  if (tier < 0) fail('INVALID_VIP_TIER', 'VIP seviyesi geçersiz.', 400);
  return withTransaction(db, () => {
    const before = db.prepare('SELECT vip_tier,vip_expires_at FROM user_game_state WHERE user_id=?').get(userId);
    if (!before) fail('USER_NOT_FOUND', 'Kullanıcı bulunamadı.', 404);
    db.prepare('UPDATE user_game_state SET vip_tier=?,vip_expires_at=NULL,updated_at=? WHERE user_id=?').run(tier, nowIso(), userId);
    const after = db.prepare('SELECT vip_tier,vip_expires_at FROM user_game_state WHERE user_id=?').get(userId);
    audit(db, { actorType: 'admin', actorId: actor, action: 'vip.assign', targetType: 'user', targetId: userId, before, after, ip });
    return after;
  });
}

export function adminLedger(db, limit = 200) {
  const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
  return db.prepare(`
    SELECT t.id,t.type,t.ref,t.idempotency_key,t.metadata,t.created_at,
      GROUP_CONCAT(a.code || ':' || e.amount, ' | ') AS entries
    FROM ledger_transactions t
    JOIN ledger_entries e ON e.transaction_id=t.id
    JOIN ledger_accounts a ON a.id=e.account_id
    GROUP BY t.id
    ORDER BY t.created_at DESC
    LIMIT ?
  `).all(safeLimit).map((row) => ({ ...row, metadata: jsonValue(row.metadata) }));
}

export function adminAuditLogs(db, limit = 200) {
  const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
  return db.prepare('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT ?').all(safeLimit).map((row) => ({
    ...row,
    before: row.before_json ? jsonValue(row.before_json) : null,
    after: row.after_json ? jsonValue(row.after_json) : null
  }));
}
