import { randomUUID } from "crypto";
import { db, timmyConfig, freeTierGrants, nostrIdentities, type TrustTier } from "@workspace/db";
import { eq, sql } from "drizzle-orm";
import { makeLogger } from "./logger.js";
import { trustService } from "./trust.js";

const logger = makeLogger("free-tier");

// ── Env-var helpers ────────────────────────────────────────────────────────────

function envInt(name: string, fallback: number): number {
  const raw = parseInt(process.env[name] ?? "", 10);
  return Number.isFinite(raw) && raw >= 0 ? raw : fallback;
}

function envFloat(name: string, fallback: number): number {
  const raw = parseFloat(process.env[name] ?? "");
  return Number.isFinite(raw) && raw >= 0 ? raw : fallback;
}

// ── Daily free budget per trust tier (sats) ───────────────────────────────────
// Override via env vars. "new" tier gets 0 — must earn trust first.
const DAILY_BUDGET_NEW         = envInt("FREE_TIER_BUDGET_NEW",         0);
const DAILY_BUDGET_ESTABLISHED = envInt("FREE_TIER_BUDGET_ESTABLISHED", 50);
const DAILY_BUDGET_TRUSTED     = envInt("FREE_TIER_BUDGET_TRUSTED",     200);
const DAILY_BUDGET_ELITE       = envInt("FREE_TIER_BUDGET_ELITE",       1_000);

// ── Generosity pool ───────────────────────────────────────────────────────────
// Pool starts at POOL_INITIAL_SATS on first boot. Grows by POOL_CREDIT_PCT of
// every paid work invoice. Never drops below 0.
const POOL_INITIAL_SATS = envInt("FREE_TIER_POOL_INITIAL_SATS", 10_000);
const POOL_CREDIT_PCT   = envFloat("FREE_TIER_POOL_CREDIT_PCT",  10);
const POOL_KEY          = "generosity_pool_sats";
const DAY_MS            = 24 * 60 * 60 * 1000;

// ── Types ─────────────────────────────────────────────────────────────────────

export interface FreeTierDecision {
  serve:       "free" | "partial" | "gate";
  absorbSats:  number;
  chargeSats:  number;
}

// ── FreeTierService ───────────────────────────────────────────────────────────

export class FreeTierService {
  // Daily sat budget for each trust tier.
  dailyBudgetForTier(tier: TrustTier): number {
    switch (tier) {
      case "elite":       return DAILY_BUDGET_ELITE;
      case "trusted":     return DAILY_BUDGET_TRUSTED;
      case "established": return DAILY_BUDGET_ESTABLISHED;
      case "new":
      default:            return DAILY_BUDGET_NEW;
    }
  }

  // Read the current pool balance; initialise to POOL_INITIAL_SATS on first call.
  async getPoolBalance(): Promise<number> {
    const rows = await db
      .select()
      .from(timmyConfig)
      .where(eq(timmyConfig.key, POOL_KEY))
      .limit(1);

    if (rows[0]) return parseInt(rows[0].value, 10) || 0;

    // First boot — seed the pool
    await db
      .insert(timmyConfig)
      .values({ key: POOL_KEY, value: String(POOL_INITIAL_SATS) })
      .onConflictDoNothing();

    logger.info("generosity pool initialised", { initialSats: POOL_INITIAL_SATS });
    return POOL_INITIAL_SATS;
  }

  /**
   * Non-mutating preview of what decide() would return, WITHOUT debiting the pool.
   * Use this for estimate/pre-flight endpoints where no execution is happening.
   * The result is indicative only — pool state may differ by the time actual decide() runs.
   */
  async decideDryRun(pubkey: string | null, estimatedSats: number): Promise<FreeTierDecision> {
    const gate: FreeTierDecision = { serve: "gate", absorbSats: 0, chargeSats: estimatedSats };

    if (!pubkey || estimatedSats <= 0) return gate;

    const identity = await trustService.getIdentityWithDecay(pubkey);
    if (!identity) return gate;

    const dailyBudget = this.dailyBudgetForTier(identity.tier);
    if (dailyBudget === 0) return gate;

    const todayAbsorbed =
      (Date.now() - identity.absorbedResetAt.getTime()) >= DAY_MS
        ? 0
        : identity.satsAbsorbedToday;
    const dailyRemaining = Math.max(0, dailyBudget - todayAbsorbed);
    if (dailyRemaining === 0) return gate;

    const poolBalance = await this.getPoolBalance();
    if (poolBalance <= 0) return gate;

    const canAbsorb = Math.min(dailyRemaining, poolBalance, estimatedSats);
    if (canAbsorb <= 0) return gate;

    if (canAbsorb >= estimatedSats) {
      return { serve: "free", absorbSats: canAbsorb, chargeSats: 0 };
    }
    return { serve: "partial", absorbSats: canAbsorb, chargeSats: estimatedSats - canAbsorb };
  }

  /**
   * Decide whether to serve a request for free, partially free, or at full cost.
   *
   * For serve="free": the pool is atomically debited for absorbSats immediately.
   *   This is correct because work starts right away — no window where the reservation
   *   can be abandoned. If work fails, call releaseReservation() to refund the pool.
   *
   * For serve="partial": the decision is advisory — pool is NOT debited yet.
   *   The user still needs to pay their portion before work starts. Pool debit happens
   *   later via reservePartialGrant() once payment is confirmed, so the pool is never
   *   depleted by a user who never pays.
   *
   * For serve="gate": no action taken (no pool debit).
   *
   * Returns:
   *   serve="free"    → absorbSats=estimatedSats (already debited), chargeSats=0
   *   serve="partial" → 0 < absorbSats < estimatedSats (advisory), chargeSats = remainder
   *   serve="gate"    → absorbSats=0, chargeSats=estimatedSats (full charge)
   */
  async decide(pubkey: string | null, estimatedSats: number): Promise<FreeTierDecision> {
    const gate: FreeTierDecision = { serve: "gate", absorbSats: 0, chargeSats: estimatedSats };

    if (!pubkey || estimatedSats <= 0) return gate;

    const identity = await trustService.getIdentityWithDecay(pubkey);
    if (!identity) return gate;

    const dailyBudget = this.dailyBudgetForTier(identity.tier);
    if (dailyBudget === 0) return gate;

    const todayAbsorbed =
      (Date.now() - identity.absorbedResetAt.getTime()) >= DAY_MS
        ? 0
        : identity.satsAbsorbedToday;
    const dailyRemaining = Math.max(0, dailyBudget - todayAbsorbed);
    if (dailyRemaining === 0) {
      logger.info("free-tier: daily budget exhausted", {
        pubkey: pubkey.slice(0, 8),
        tier: identity.tier,
        todayAbsorbed,
        dailyBudget,
      });
      return gate;
    }

    const poolBalance = await this.getPoolBalance();
    if (poolBalance <= 0) {
      logger.warn("free-tier: pool empty", { pubkey: pubkey.slice(0, 8) });
      return gate;
    }

    const canAbsorb = Math.min(dailyRemaining, poolBalance, estimatedSats);
    if (canAbsorb <= 0) return gate;

    if (canAbsorb >= estimatedSats) {
      // Fully free — atomically debit the pool now, work starts immediately after this.
      const debited = await this._atomicPoolDebit(canAbsorb);
      if (debited <= 0) {
        // Pool drained between balance read and debit (concurrent request)
        logger.warn("free-tier: pool drained between check and debit", { pubkey: pubkey.slice(0, 8) });
        return gate;
      }
      if (debited >= estimatedSats) {
        // Pool covered the full cost — fully free.
        logger.info("free-tier: reserved free (pool debited)", {
          pubkey: pubkey.slice(0, 8),
          tier: identity.tier,
          absorbSats: debited,
        });
        return { serve: "free", absorbSats: debited, chargeSats: 0 };
      }
      // Pool covered only part of the cost — downgrade to partial.
      // Pool has already been debited for `debited` sats; return it (no advisory hold;
      // partial path will re-reserve at payment time). Release and fall through.
      void this.releaseReservation(debited, "free downgraded to partial due to pool race");
      // fall through to return partial advisory below
      const chargeSats = estimatedSats - debited; // advisory, not actually charged yet
      logger.info("free-tier: downgraded free→partial (pool shrank under race)", {
        pubkey: pubkey.slice(0, 8),
        tier: identity.tier,
        debited,
        chargeSats,
      });
      return { serve: "partial", absorbSats: debited, chargeSats };
    }

    // Partial: advisory only — pool debit deferred until payment confirmed.
    logger.info("free-tier: partial subsidy (advisory, pool debit deferred)", {
      pubkey: pubkey.slice(0, 8),
      tier: identity.tier,
      absorbSats: canAbsorb,
      chargeSats: estimatedSats - canAbsorb,
    });
    return { serve: "partial", absorbSats: canAbsorb, chargeSats: estimatedSats - canAbsorb };
  }

  /**
   * Atomically debit the generosity pool inside a transaction with row-level lock.
   * Returns the actual amount debited (may be less than requested if pool is low).
   * Returns 0 if pool has insufficient funds.
   */
  private async _atomicPoolDebit(requestedSats: number): Promise<number> {
    if (requestedSats <= 0) return 0;

    let actualDebited = 0;

    await db.transaction(async (tx) => {
      // Ensure pool row exists
      await tx
        .insert(timmyConfig)
        .values({ key: POOL_KEY, value: String(POOL_INITIAL_SATS) })
        .onConflictDoNothing();

      // Lock pool row for the duration of this transaction
      const locked = await tx.execute(
        sql`SELECT value::int AS balance FROM timmy_config WHERE key = ${POOL_KEY} FOR UPDATE`,
      );
      const poolBalance = (locked.rows[0] as { balance: number } | undefined)?.balance ?? 0;
      if (poolBalance <= 0) return; // pool empty, actualDebited stays 0

      actualDebited = Math.min(requestedSats, poolBalance);
      const newPoolBalance = poolBalance - actualDebited;
      const now = new Date();

      await tx
        .update(timmyConfig)
        .set({ value: String(newPoolBalance), updatedAt: now })
        .where(eq(timmyConfig.key, POOL_KEY));
    });

    return actualDebited;
  }

  /**
   * Atomically reserve and debit the pool for a partial grant at payment confirmation time.
   * Called in advanceJob() when work payment is confirmed, BEFORE starting work.
   *
   * Returns the actual sats debited (may be less than requested if pool drained since decide()).
   * If returns 0, pool is now empty — caller should recalculate charge or gate the request.
   */
  async reservePartialGrant(requestedAbsorbSats: number, pubkey: string): Promise<number> {
    if (requestedAbsorbSats <= 0) return 0;

    // Re-check daily limits before debiting (advisory decide() could be stale)
    const identity = await trustService.getIdentityWithDecay(pubkey);
    if (!identity) return 0;

    const dailyBudget = this.dailyBudgetForTier(identity.tier);
    const todayAbsorbed =
      (Date.now() - identity.absorbedResetAt.getTime()) >= DAY_MS
        ? 0
        : identity.satsAbsorbedToday;
    const dailyRemaining = Math.max(0, dailyBudget - todayAbsorbed);
    const canAbsorb = Math.min(requestedAbsorbSats, dailyRemaining);
    if (canAbsorb <= 0) return 0;

    const debited = await this._atomicPoolDebit(canAbsorb);
    logger.info("free-tier: partial grant reserved at payment time", {
      pubkey: pubkey.slice(0, 8),
      requestedAbsorbSats,
      canAbsorb,
      debited,
    });
    return debited;
  }

  /**
   * Release a pool reservation when a request is rejected, fails, or is not executed.
   * Returns `absorbSats` to the generosity pool.
   * Only call if decide() debited the pool (serve="free" path).
   */
  async releaseReservation(absorbSats: number, reason: string): Promise<void> {
    if (absorbSats <= 0) return;
    const now = new Date();
    await db
      .update(timmyConfig)
      .set({
        value: sql`(value::int + ${absorbSats})::text`,
        updatedAt: now,
      })
      .where(eq(timmyConfig.key, POOL_KEY));
    logger.info("free-tier: reservation released", { absorbSats, reason });
  }

  /**
   * Credit the generosity pool from a paid interaction.
   * Called after a work invoice is paid and work completes successfully.
   * creditSats = paidSats * POOL_CREDIT_PCT / 100
   */
  async credit(paidSats: number): Promise<void> {
    if (paidSats <= 0) return;
    const creditSats = Math.floor(paidSats * POOL_CREDIT_PCT / 100);
    if (creditSats <= 0) return;

    const now = new Date();
    await db
      .insert(timmyConfig)
      .values({ key: POOL_KEY, value: String(creditSats), updatedAt: now })
      .onConflictDoUpdate({
        target: timmyConfig.key,
        set: {
          value: sql`(timmy_config.value::int + ${creditSats})::text`,
          updatedAt: now,
        },
      });

    logger.info("generosity pool credited", { paidSats, creditSats });
  }

  /**
   * Record the audit log entry and update daily absorption counters AFTER work completes.
   *
   * The pool was already debited (by decide() for free jobs, by reservePartialGrant()
   * for partial jobs). This call only writes the audit row and updates daily absorption.
   *
   * `actualAbsorbed` = actual cost in sats (may be less than reservedAbsorbed due to
   *   actual token usage being lower than estimate).
   * `reservedAbsorbed` = amount debited from pool at reservation time.
   * Any over-reservation (reservedAbsorbed - actualAbsorbed) is released back to pool.
   */
  async recordGrant(
    pubkey: string,
    requestHash: string,
    actualAbsorbed: number,
    reservedAbsorbed: number,
  ): Promise<void> {
    const toRecord = Math.max(0, actualAbsorbed);
    const overReserved = Math.max(0, reservedAbsorbed - toRecord);
    if (toRecord <= 0 && overReserved <= 0) return;

    const now = new Date();

    await db.transaction(async (tx) => {
      // Return over-reservation to pool (estimated > actual cost)
      if (overReserved > 0) {
        await tx
          .update(timmyConfig)
          .set({
            value: sql`(value::int + ${overReserved})::text`,
            updatedAt: now,
          })
          .where(eq(timmyConfig.key, POOL_KEY));
      }

      if (toRecord > 0) {
        // Update identity daily absorption (CASE handles new-day reset atomically)
        await tx
          .update(nostrIdentities)
          .set({
            satsAbsorbedToday: sql`
              CASE
                WHEN EXTRACT(EPOCH FROM (${now}::timestamptz - absorbed_reset_at)) * 1000 >= ${DAY_MS}
                THEN ${toRecord}
                ELSE sats_absorbed_today + ${toRecord}
              END
            `,
            absorbedResetAt: sql`
              CASE
                WHEN EXTRACT(EPOCH FROM (${now}::timestamptz - absorbed_reset_at)) * 1000 >= ${DAY_MS}
                THEN ${now}::timestamptz
                ELSE absorbed_reset_at
              END
            `,
            updatedAt: now,
          })
          .where(eq(nostrIdentities.pubkey, pubkey));

        const poolRows = await tx.select().from(timmyConfig).where(eq(timmyConfig.key, POOL_KEY)).limit(1);
        const poolBalanceAfter = poolRows[0] ? parseInt(poolRows[0].value, 10) : 0;

        await tx.insert(freeTierGrants).values({
          id: randomUUID(),
          pubkey,
          requestHash,
          satsAbsorbed: toRecord,
          poolBalanceAfter,
        });
      }
    });

    logger.info("free-tier grant recorded", {
      pubkey: pubkey.slice(0, 8),
      actualAbsorbed: toRecord,
      reservedAbsorbed,
      overReserved,
    });
  }

  /**
   * Pool status snapshot — for health/metrics endpoints.
   */
  async poolStatus(): Promise<{
    balanceSats: number;
    budgets: Record<TrustTier, number>;
  }> {
    const balanceSats = await this.getPoolBalance();
    return {
      balanceSats,
      budgets: {
        new:         DAILY_BUDGET_NEW,
        established: DAILY_BUDGET_ESTABLISHED,
        trusted:     DAILY_BUDGET_TRUSTED,
        elite:       DAILY_BUDGET_ELITE,
      },
    };
  }
}

export const freeTierService = new FreeTierService();
