import crypto from 'node:crypto';

// Hardcoded fallback catalog used when h5_plan_catalog table is not yet seeded.
// priceCents: monthly price in CNY cents (0 = free)
// periodTokens: token allowance per period (0 = unlimited)
// periodImages: image generation quota per period (0 = unlimited)
// modelTier: 'basic' | 'standard' | 'premium'
// overageRate: cost multiplier when quota exhausted (1.0 = no discount)
// periodDays: rolling period length in days
export const PLAN_CATALOG = {
  free: {
    name: '免费版',
    priceCents: 0,
    periodTokens: 150_000,
    periodImages: 10,
    modelTier: 'basic',
    overageRate: 1.00,
    periodDays: 30,
  },
  lite: {
    name: '轻量版',
    priceCents: 990,
    periodTokens: 1_200_000,
    periodImages: 50,
    modelTier: 'basic',
    overageRate: 0.50,
    periodDays: 30,
  },
  standard: {
    name: '标准版',
    priceCents: 2900,
    periodTokens: 4_500_000,
    periodImages: 200,
    modelTier: 'standard',
    overageRate: 0.70,
    periodDays: 30,
  },
  pro: {
    name: '专业版',
    priceCents: 7900,
    periodTokens: 0,
    periodImages: 0,
    modelTier: 'premium',
    overageRate: 0.80,
    periodDays: 30,
  },
};

export function getPlanDef(planType) {
  return PLAN_CATALOG[planType] ?? null;
}

// Tier order: higher index = higher plan. Used for upgrade/downgrade checks.
const PLAN_ORDER = Object.fromEntries(
  Object.keys(PLAN_CATALOG).map((key, i) => [key, i]),
);

// How many "typical calls" a token budget represents (for display purposes).
const AVG_TOKENS_PER_CALL = 3000;
export function tokensToCallsApprox(tokens) {
  if (tokens === 0) return null; // unlimited
  return Math.floor(tokens / AVG_TOKENS_PER_CALL);
}

function mapSubRow(row) {
  if (!row) return null;
  return {
    id: row.id,
    userId: row.user_id,
    planType: row.plan_type,
    status: row.status,
    periodTokensLimit: Number(row.period_tokens_limit ?? 0),
    periodTokensUsed: Number(row.period_tokens_used ?? 0),
    periodImagesLimit: Number(row.period_images_limit ?? 0),
    periodImagesUsed: Number(row.period_images_used ?? 0),
    periodStart: Number(row.period_start),
    periodEnd: Number(row.period_end),
    expiresAt: Number(row.expires_at),
    overageRate: Number(row.overage_rate ?? 1.0),
    autoRenew: Boolean(row.auto_renew),
    operatorId: row.operator_id ?? null,
    note: row.note ?? null,
    createdAt: Number(row.created_at),
    updatedAt: Number(row.updated_at),
  };
}

export function createSubscriptionService(pool, { getPlanAsync = null } = {}) {
  // Resolve plan definition: prefer DB-backed loader, fall back to hardcoded catalog.
  const resolvePlan = async (planType) => {
    if (getPlanAsync) {
      const p = await getPlanAsync(planType);
      if (p) return p;
    }
    const p = getPlanDef(planType);
    return p ? { ...p, periodImages: p.periodImages ?? 0 } : null;
  };

  const getActiveSubscription = async (userId) => {
    const now = Date.now();
    const [rows] = await pool.query(
      `SELECT * FROM h5_subscriptions
       WHERE user_id = ? AND status = 'active' AND expires_at > ?
       ORDER BY expires_at DESC LIMIT 1`,
      [userId, now],
    );
    return mapSubRow(rows[0]);
  };

  // Grant a new subscription (admin or system action). Cancels the current active one.
  const grantSubscription = async (userId, planType, durationDays, operatorId = null, note = '') => {
    const plan = await resolvePlan(planType);
    if (!plan) return { ok: false, message: `未知套餐类型: ${planType}` };

    const now = Date.now();
    const periodDays = durationDays ?? plan.periodDays;
    const periodEnd = now + periodDays * 24 * 60 * 60 * 1000;

    const conn = await pool.getConnection();
    try {
      await conn.beginTransaction();

      // Cancel any existing active subscription
      await conn.query(
        `UPDATE h5_subscriptions SET status = 'cancelled', updated_at = ?
         WHERE user_id = ? AND status = 'active'`,
        [now, userId],
      );

      const id = crypto.randomUUID();
      await conn.query(
        `INSERT INTO h5_subscriptions
         (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used,
          period_images_limit, period_images_used,
          period_start, period_end, expires_at, overage_rate, auto_renew,
          operator_id, note, created_at, updated_at)
         VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, 0, ?, ?, ?, ?)`,
        [
          id, userId, planType, plan.periodTokens,
          plan.periodImages ?? 0,
          now, periodEnd, periodEnd,
          Number(plan.overageRate.toFixed(2)),
          operatorId, note || null, now, now,
        ],
      );

      await conn.query(
        `UPDATE h5_users SET plan_type = ?, updated_at = ? WHERE id = ?`,
        [planType, now, userId],
      );

      await conn.commit();
      const [rows] = await pool.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [id]);
      return { ok: true, subscription: mapSubRow(rows[0]) };
    } catch (err) {
      await conn.rollback();
      throw err;
    } finally {
      conn.release();
    }
  };

  // Called inside an existing transaction (conn already in transaction).
  // Deducts deltaTokens from the active subscription quota if available.
  // Returns { fullyCovers: bool, overageRate: number }
  const consumeQuota = async (userId, deltaTokens, conn) => {
    if (!deltaTokens || deltaTokens <= 0) {
      return { fullyCovers: true, overageRate: 1.0 };
    }
    const now = Date.now();
    const [rows] = await conn.query(
      `SELECT * FROM h5_subscriptions
       WHERE user_id = ? AND status = 'active' AND expires_at > ?
       ORDER BY expires_at DESC LIMIT 1
       FOR UPDATE`,
      [userId, now],
    );
    const sub = mapSubRow(rows[0]);
    if (!sub) return { fullyCovers: false, overageRate: 1.0 };

    const unlimited = sub.periodTokensLimit === 0;
    const remaining = unlimited ? Infinity : sub.periodTokensLimit - sub.periodTokensUsed;

    if (unlimited || remaining >= deltaTokens) {
      await conn.query(
        `UPDATE h5_subscriptions
         SET period_tokens_used = period_tokens_used + ?, updated_at = ?
         WHERE id = ?`,
        [deltaTokens, now, sub.id],
      );
      return { fullyCovers: true, overageRate: sub.overageRate };
    }

    // Partially exhausted — consume remainder, overage applies to whole request for simplicity
    if (remaining > 0) {
      await conn.query(
        `UPDATE h5_subscriptions
         SET period_tokens_used = period_tokens_limit, updated_at = ?
         WHERE id = ?`,
        [now, sub.id],
      );
    }
    return { fullyCovers: false, overageRate: sub.overageRate };
  };

  // Renew an existing subscription for another period (same plan).
  // Used by a scheduled worker or manual admin action.
  const renewSubscription = async (subId, operatorId = null) => {
    const [rows] = await pool.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [subId]);
    const sub = mapSubRow(rows[0]);
    if (!sub) return { ok: false, message: '订阅不存在' };

    const plan = await resolvePlan(sub.planType);
    if (!plan) return { ok: false, message: '套餐定义已失效' };

    const now = Date.now();
    const newPeriodEnd = now + plan.periodDays * 24 * 60 * 60 * 1000;

    const conn = await pool.getConnection();
    try {
      await conn.beginTransaction();
      const newId = crypto.randomUUID();
      await conn.query(
        `UPDATE h5_subscriptions SET status = 'expired', updated_at = ? WHERE id = ?`,
        [now, subId],
      );
      await conn.query(
        `INSERT INTO h5_subscriptions
         (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used,
          period_images_limit, period_images_used,
          period_start, period_end, expires_at, overage_rate, auto_renew,
          operator_id, note, created_at, updated_at)
         VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        [
          newId, sub.userId, sub.planType, plan.periodTokens,
          plan.periodImages ?? 0,
          now, newPeriodEnd, newPeriodEnd,
          Number(plan.overageRate.toFixed(2)),
          sub.autoRenew ? 1 : 0,
          operatorId, '自动续订', now, now,
        ],
      );
      await conn.commit();
      const [newRows] = await pool.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [newId]);
      return { ok: true, subscription: mapSubRow(newRows[0]) };
    } catch (err) {
      await conn.rollback();
      throw err;
    } finally {
      conn.release();
    }
  };

  // Mark expired subscriptions and reset plan_type to 'free'.
  // Call periodically from a worker.
  const expireStaleSubscriptions = async () => {
    const now = Date.now();
    const [result] = await pool.query(
      `UPDATE h5_subscriptions SET status = 'expired', updated_at = ?
       WHERE status = 'active' AND expires_at <= ?`,
      [now, now],
    );
    if (result.affectedRows > 0) {
      await pool.query(
        `UPDATE h5_users u
         LEFT JOIN h5_subscriptions s
           ON s.user_id = u.id AND s.status = 'active' AND s.expires_at > ?
         SET u.plan_type = 'free', u.updated_at = ?
         WHERE u.plan_type != 'free' AND s.id IS NULL`,
        [now, now],
      );
    }
    return result.affectedRows;
  };

  const cancelSubscription = async (userId, operatorId = null) => {
    const now = Date.now();
    const [result] = await pool.query(
      `UPDATE h5_subscriptions SET status = 'cancelled', updated_at = ?
       WHERE user_id = ? AND status = 'active'`,
      [now, userId],
    );
    if (result.affectedRows > 0) {
      await pool.query(
        `UPDATE h5_users SET plan_type = 'free', updated_at = ? WHERE id = ?`,
        [now, userId],
      );
    }
    return { ok: true, cancelled: result.affectedRows > 0 };
  };

  const listSubscriptions = async ({ userId = null, status = null, page = 1, pageSize = 20 } = {}) => {
    const safePageSize = Math.min(Math.max(Number(pageSize) || 20, 1), 100);
    const safePage = Math.max(Number(page) || 1, 1);
    const offset = (safePage - 1) * safePageSize;
    const params = [];
    const clauses = [];
    if (userId) { clauses.push('s.user_id = ?'); params.push(userId); }
    if (status) { clauses.push('s.status = ?'); params.push(status); }
    const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
    const [[{ total }]] = await pool.query(
      `SELECT COUNT(*) AS total FROM h5_subscriptions s ${where}`, params,
    );
    const [rows] = await pool.query(
      `SELECT s.*, u.username, u.display_name
       FROM h5_subscriptions s JOIN h5_users u ON u.id = s.user_id
       ${where} ORDER BY s.created_at DESC LIMIT ${safePageSize} OFFSET ${offset}`,
      params,
    );
    return {
      total: Number(total),
      page: safePage,
      pageSize: safePageSize,
      items: rows.map((r) => ({
        ...mapSubRow(r),
        username: r.username,
        displayName: r.display_name,
      })),
    };
  };

  // User self-service: purchase/upgrade a subscription by deducting from wallet balance.
  // Upgrades are always allowed. Downgrades are blocked while an active subscription exists.
  // autoRenew: whether to auto-charge and renew when the subscription expires.
  const purchaseSubscription = async (userId, planType, autoRenew = false) => {
    const plan = await resolvePlan(planType);
    if (!plan || plan.priceCents === 0) {
      return { ok: false, message: '无效套餐或免费套餐不可购买' };
    }

    const now = Date.now();
    const conn = await pool.getConnection();
    try {
      await conn.beginTransaction();

      // Check current subscription (with row lock) to enforce upgrade-only rule.
      const [currentSubRows] = await conn.query(
        `SELECT * FROM h5_subscriptions
         WHERE user_id = ? AND status = 'active' AND expires_at > ?
         ORDER BY expires_at DESC LIMIT 1 FOR UPDATE`,
        [userId, now],
      );
      const currentSub = mapSubRow(currentSubRows[0]);
      if (currentSub) {
        const currentOrder = PLAN_ORDER[currentSub.planType] ?? 0;
        const newOrder = PLAN_ORDER[planType] ?? 0;
        if (newOrder < currentOrder) {
          await conn.rollback();
          return {
            ok: false,
            code: 'DOWNGRADE_NOT_ALLOWED',
            message: '当前套餐有效期内不可降级，到期后将自动降为免费版',
            currentPlanType: currentSub.planType,
          };
        }
      }

      const [walletRows] = await conn.query(
        `SELECT balance_cents FROM h5_user_wallets WHERE user_id = ? FOR UPDATE`,
        [userId],
      );
      const balanceCents = Number(walletRows[0]?.balance_cents ?? 0);

      if (balanceCents < plan.priceCents) {
        await conn.rollback();
        return {
          ok: false,
          code: 'INSUFFICIENT_BALANCE',
          message: '余额不足，请充值后订阅',
          balanceCents,
          requiredCents: plan.priceCents,
          shortfallCents: plan.priceCents - balanceCents,
        };
      }

      await conn.query(
        `UPDATE h5_user_wallets SET balance_cents = balance_cents - ?, updated_at = ? WHERE user_id = ?`,
        [plan.priceCents, now, userId],
      );

      await conn.query(
        `INSERT INTO h5_billing_ledger (user_id, type, amount_cents, tokens, note, operator_id, created_at)
         VALUES (?, 'deduct', ?, 0, ?, NULL, ?)`,
        [userId, plan.priceCents, `subscription:${planType}`, now],
      );

      await conn.query(
        `UPDATE h5_subscriptions SET status = 'cancelled', updated_at = ?
         WHERE user_id = ? AND status = 'active'`,
        [now, userId],
      );

      const id = crypto.randomUUID();
      const periodEnd = now + plan.periodDays * 24 * 60 * 60 * 1000;
      await conn.query(
        `INSERT INTO h5_subscriptions
         (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used,
          period_images_limit, period_images_used,
          period_start, period_end, expires_at, overage_rate, auto_renew,
          operator_id, note, created_at, updated_at)
         VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, ?, NULL, '用户自助购买', ?, ?)`,
        [
          id, userId, planType, plan.periodTokens,
          plan.periodImages ?? 0,
          now, periodEnd, periodEnd,
          Number(plan.overageRate.toFixed(2)),
          autoRenew ? 1 : 0,
          now, now,
        ],
      );

      await conn.query(
        `UPDATE h5_users SET plan_type = ?, updated_at = ? WHERE id = ?`,
        [planType, now, userId],
      );

      await conn.commit();
      const [subRows] = await pool.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [id]);
      return {
        ok: true,
        subscription: mapSubRow(subRows[0]),
        balanceCents: balanceCents - plan.priceCents,
      };
    } catch (err) {
      await conn.rollback();
      throw err;
    } finally {
      conn.release();
    }
  };

  // Toggle auto-renew on the user's current active subscription.
  const setAutoRenew = async (userId, enabled) => {
    const now = Date.now();
    const [result] = await pool.query(
      `UPDATE h5_subscriptions SET auto_renew = ?, updated_at = ?
       WHERE user_id = ? AND status = 'active' AND expires_at > ?`,
      [enabled ? 1 : 0, now, userId, now],
    );
    return { ok: true, updated: result.affectedRows > 0 };
  };

  // Process auto-renewals for subscriptions that just expired with auto_renew = 1.
  // Should be called BEFORE expireStaleSubscriptions in the periodic worker.
  const processAutoRenewals = async () => {
    const now = Date.now();
    // Find active subscriptions that have expired with auto_renew on.
    const [rows] = await pool.query(
      `SELECT s.*, w.balance_cents
       FROM h5_subscriptions s
       JOIN h5_user_wallets w ON w.user_id = s.user_id
       WHERE s.status = 'active' AND s.auto_renew = 1 AND s.expires_at <= ?`,
      [now],
    );

    let renewed = 0;
    let failed = 0;

    for (const row of rows) {
      const sub = mapSubRow(row);
      const plan = getPlanDef(sub.planType);
      if (!plan || plan.priceCents === 0) continue;

      const balance = Number(row.balance_cents ?? 0);
      const conn = await pool.getConnection();
      try {
        await conn.beginTransaction();

        // Re-lock wallet and verify balance
        const [walletRows] = await conn.query(
          `SELECT balance_cents FROM h5_user_wallets WHERE user_id = ? FOR UPDATE`,
          [sub.userId],
        );
        const balanceCents = Number(walletRows[0]?.balance_cents ?? 0);

        if (balanceCents < plan.priceCents) {
          // Not enough — let expireStaleSubscriptions handle the reset
          await conn.rollback();
          failed++;
          console.log(`Auto-renew failed for user ${sub.userId} (${sub.planType}): insufficient balance`);
          continue;
        }

        // Deduct wallet
        await conn.query(
          `UPDATE h5_user_wallets SET balance_cents = balance_cents - ?, updated_at = ? WHERE user_id = ?`,
          [plan.priceCents, now, sub.userId],
        );
        await conn.query(
          `INSERT INTO h5_billing_ledger (user_id, type, amount_cents, tokens, note, operator_id, created_at)
           VALUES (?, 'deduct', ?, 0, ?, NULL, ?)`,
          [sub.userId, plan.priceCents, `auto_renew:${sub.planType}`, now],
        );

        // Expire old subscription
        await conn.query(
          `UPDATE h5_subscriptions SET status = 'expired', auto_renew = 0, updated_at = ? WHERE id = ?`,
          [now, sub.id],
        );

        // Create new subscription
        const newId = crypto.randomUUID();
        const newPeriodEnd = now + plan.periodDays * 24 * 60 * 60 * 1000;
        await conn.query(
          `INSERT INTO h5_subscriptions
           (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used,
            period_images_limit, period_images_used,
            period_start, period_end, expires_at, overage_rate, auto_renew,
            operator_id, note, created_at, updated_at)
           VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, 1, NULL, '自动续费', ?, ?)`,
          [
            newId, sub.userId, sub.planType, plan.periodTokens,
            plan.periodImages ?? 0,
            now, newPeriodEnd, newPeriodEnd,
            Number(plan.overageRate.toFixed(2)),
            now, now,
          ],
        );

        // Keep user's plan_type current
        await conn.query(
          `UPDATE h5_users SET plan_type = ?, updated_at = ? WHERE id = ?`,
          [sub.planType, now, sub.userId],
        );

        await conn.commit();
        renewed++;
        console.log(`Auto-renewed ${sub.planType} for user ${sub.userId}`);
      } catch (err) {
        await conn.rollback();
        console.warn(`Auto-renew error for user ${sub.userId}:`, err);
        failed++;
      } finally {
        conn.release();
      }
    }

    return { renewed, failed };
  };

  const consumeImageQuotaTx = async (userId, count, conn) => {
    if (!count || count <= 0) return { fullyCovers: true };
    const now = Date.now();
    const [rows] = await conn.query(
      `SELECT * FROM h5_subscriptions
       WHERE user_id = ? AND status = 'active' AND expires_at > ?
       ORDER BY expires_at DESC LIMIT 1 FOR UPDATE`,
      [userId, now],
    );
    const sub = mapSubRow(rows[0]);
    if (!sub) return { fullyCovers: false };

    const unlimited = sub.periodImagesLimit === 0;
    const remaining = unlimited ? Infinity : sub.periodImagesLimit - sub.periodImagesUsed;

    if (unlimited || remaining >= count) {
      await conn.query(
        `UPDATE h5_subscriptions
         SET period_images_used = period_images_used + ?, updated_at = ?
         WHERE id = ?`,
        [count, now, sub.id],
      );
      return { fullyCovers: true };
    }
    return { fullyCovers: false };
  };

  // Deduct image count from the active subscription quota.
  // If conn is omitted, this method manages its own short transaction.
  const consumeImageQuota = async (userId, count, conn = null) => {
    if (conn) return consumeImageQuotaTx(userId, count, conn);

    const ownConn = await pool.getConnection();
    try {
      await ownConn.beginTransaction();
      const result = await consumeImageQuotaTx(userId, count, ownConn);
      await ownConn.commit();
      return result;
    } catch (err) {
      await ownConn.rollback();
      throw err;
    } finally {
      ownConn.release();
    }
  };

  return {
    getActiveSubscription,
    grantSubscription,
    purchaseSubscription,
    setAutoRenew,
    processAutoRenewals,
    consumeQuota,
    consumeImageQuota,
    renewSubscription,
    expireStaleSubscriptions,
    cancelSubscription,
    listSubscriptions,
  };
}

// ── Plan Catalog (DB-backed) ─────────────────────────────────────────────────

// Ensure h5_plan_catalog table exists, add image columns to h5_subscriptions,
// and seed default plans from PLAN_CATALOG if the table is empty.
export async function ensurePlanCatalogSchema(pool) {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS h5_plan_catalog (
      plan_type  VARCHAR(32) PRIMARY KEY,
      name       VARCHAR(64) NOT NULL,
      price_cents INT NOT NULL DEFAULT 0,
      period_days INT NOT NULL DEFAULT 30,
      period_tokens BIGINT NOT NULL DEFAULT 0,
      period_images INT NOT NULL DEFAULT 0,
      model_tier VARCHAR(32) NOT NULL DEFAULT 'basic',
      overage_rate DECIMAL(4,2) NOT NULL DEFAULT 1.00,
      sort_order INT NOT NULL DEFAULT 0,
      is_active TINYINT(1) NOT NULL DEFAULT 1,
      description VARCHAR(512) NULL,
      created_at BIGINT NOT NULL,
      updated_at BIGINT NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  `);

  // Idempotently add image quota columns to h5_subscriptions.
  for (const col of [
    'ALTER TABLE h5_subscriptions ADD COLUMN period_images_limit INT NOT NULL DEFAULT 0',
    'ALTER TABLE h5_subscriptions ADD COLUMN period_images_used INT NOT NULL DEFAULT 0',
  ]) {
    try { await pool.query(col); } catch (_) { /* column already exists */ }
  }

  // Seed defaults if catalog is empty.
  const [[{ cnt }]] = await pool.query(`SELECT COUNT(*) AS cnt FROM h5_plan_catalog`);
  if (Number(cnt) === 0) {
    const now = Date.now();
    const entries = Object.entries(PLAN_CATALOG);
    for (const [i, [planType, plan]] of entries.entries()) {
      await pool.query(
        `INSERT IGNORE INTO h5_plan_catalog
         (plan_type, name, price_cents, period_days, period_tokens, period_images,
          model_tier, overage_rate, sort_order, is_active, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`,
        [
          planType, plan.name, plan.priceCents, plan.periodDays,
          plan.periodTokens, plan.periodImages ?? 0,
          plan.modelTier, plan.overageRate, i, now, now,
        ],
      );
    }
  }
}

export function createPlanCatalogService(pool) {
  function mapPlanRow(row) {
    return {
      planType: row.plan_type,
      name: row.name,
      priceCents: Number(row.price_cents),
      periodDays: Number(row.period_days),
      periodTokens: Number(row.period_tokens),
      periodImages: Number(row.period_images),
      modelTier: row.model_tier,
      overageRate: Number(row.overage_rate),
      sortOrder: Number(row.sort_order),
      isActive: Boolean(row.is_active),
      description: row.description ?? null,
    };
  }

  const listPlans = async ({ includeInactive = true } = {}) => {
    const where = includeInactive ? '' : 'WHERE is_active = 1';
    const [rows] = await pool.query(
      `SELECT * FROM h5_plan_catalog ${where} ORDER BY sort_order ASC, price_cents ASC`,
    );
    return rows.map(mapPlanRow);
  };

  const getPlan = async (planType) => {
    const [rows] = await pool.query(
      `SELECT * FROM h5_plan_catalog WHERE plan_type = ?`, [planType],
    );
    return rows[0] ? mapPlanRow(rows[0]) : null;
  };

  const upsertPlan = async (planType, data) => {
    if (!planType || !/^[a-z0-9_]{1,32}$/.test(planType)) {
      return { ok: false, message: '套餐标识只能使用小写字母、数字和下划线，最长32位' };
    }
    const now = Date.now();
    await pool.query(
      `INSERT INTO h5_plan_catalog
       (plan_type, name, price_cents, period_days, period_tokens, period_images,
        model_tier, overage_rate, sort_order, is_active, description, created_at, updated_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
       ON DUPLICATE KEY UPDATE
         name = VALUES(name), price_cents = VALUES(price_cents),
         period_days = VALUES(period_days), period_tokens = VALUES(period_tokens),
         period_images = VALUES(period_images), model_tier = VALUES(model_tier),
         overage_rate = VALUES(overage_rate), sort_order = VALUES(sort_order),
         is_active = VALUES(is_active), description = VALUES(description),
         updated_at = VALUES(updated_at)`,
      [
        planType,
        String(data.name ?? '').trim() || planType,
        Math.max(0, Math.round(Number(data.priceCents ?? 0))),
        Math.max(1, Math.round(Number(data.periodDays ?? 30))),
        Math.max(0, Math.round(Number(data.periodTokens ?? 0))),
        Math.max(0, Math.round(Number(data.periodImages ?? 0))),
        ['basic', 'standard', 'premium'].includes(data.modelTier) ? data.modelTier : 'basic',
        Math.min(2, Math.max(0, Number((data.overageRate ?? 1.0).toFixed(2)))),
        Math.round(Number(data.sortOrder ?? 0)),
        data.isActive !== false ? 1 : 0,
        data.description ? String(data.description).trim() : null,
        now, now,
      ],
    );
    return { ok: true, plan: await getPlan(planType) };
  };

  const deletePlan = async (planType) => {
    if (planType === 'free') return { ok: false, message: '免费套餐不可删除' };
    const [result] = await pool.query(
      `DELETE FROM h5_plan_catalog WHERE plan_type = ?`, [planType],
    );
    return { ok: true, deleted: result.affectedRows > 0 };
  };

  return { listPlans, getPlan, upsertPlan, deletePlan };
}
