import { createHmac, randomBytes } from "crypto";
import { db, nostrIdentities, type NostrIdentity, type TrustTier } from "@workspace/db";
import { eq } from "drizzle-orm";
import { makeLogger } from "./logger.js";
import { relayAccountService } from "./relay-accounts.js";

const logger = makeLogger("trust");

// ── 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;
}

// ── Tier score boundaries (inclusive lower bound) ─────────────────────────────
// Override with TRUST_TIER_ESTABLISHED, TRUST_TIER_TRUSTED, TRUST_TIER_ELITE.

const TIER_ESTABLISHED = envInt("TRUST_TIER_ESTABLISHED", 10);
const TIER_TRUSTED     = envInt("TRUST_TIER_TRUSTED",     50);
const TIER_ELITE       = envInt("TRUST_TIER_ELITE",       200);

// Points per event
const SCORE_PER_SUCCESS = envInt("TRUST_SCORE_PER_SUCCESS", 2);
const SCORE_PER_FAILURE = envInt("TRUST_SCORE_PER_FAILURE", 5);

// Soft decay: points lost per day absent, applied lazily on read
const DECAY_ABSENT_DAYS = envInt("TRUST_DECAY_ABSENT_DAYS", 30);
const DECAY_PER_DAY     = envInt("TRUST_DECAY_PER_DAY", 1);

// ── HMAC token for nostr_token auth ──────────────────────────────────────────
// Token format: `{pubkey}:{expiry}:{hmac}`

const TOKEN_SECRET: string = (() => {
  const s = process.env["TIMMY_TOKEN_SECRET"];
  if (s && s.length >= 32) return s;
  const generated = randomBytes(32).toString("hex");
  logger.warn("TIMMY_TOKEN_SECRET not set — generated ephemeral secret (tokens expire on restart)");
  return generated;
})();

const TOKEN_TTL_SECS = envInt("NOSTR_TOKEN_TTL_SECS", 86400); // 24 h

function signToken(pubkey: string, expiry: number): string {
  const payload = `${pubkey}:${expiry}`;
  const hmac = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex");
  return `${payload}:${hmac}`;
}

export function verifyToken(token: string): { pubkey: string; expiry: number } | null {
  const parts = token.split(":");
  if (parts.length !== 3) return null;
  const [pubkey, expiryStr, hmac] = parts as [string, string, string];
  const expiry = parseInt(expiryStr, 10);
  if (!Number.isFinite(expiry) || Date.now() / 1000 > expiry) return null;
  const expected = createHmac("sha256", TOKEN_SECRET)
    .update(`${pubkey}:${expiry}`)
    .digest("hex");
  if (expected !== hmac) return null;
  return { pubkey, expiry };
}

export function issueToken(pubkey: string): string {
  const expiry = Math.floor(Date.now() / 1000) + TOKEN_TTL_SECS;
  return signToken(pubkey, expiry);
}

// ── Trust score helpers ───────────────────────────────────────────────────────

function computeTier(score: number): TrustTier {
  if (score >= TIER_ELITE)       return "elite";
  if (score >= TIER_TRUSTED)     return "trusted";
  if (score >= TIER_ESTABLISHED) return "established";
  return "new";
}

function applyDecay(identity: NostrIdentity): number {
  const daysSeen =
    (Date.now() - identity.lastSeen.getTime()) / (1000 * 60 * 60 * 24);
  if (daysSeen < DECAY_ABSENT_DAYS) return identity.trustScore;
  const daysAbsent = Math.floor(daysSeen - DECAY_ABSENT_DAYS);
  return Math.max(0, identity.trustScore - daysAbsent * DECAY_PER_DAY);
}

// ── TrustService ──────────────────────────────────────────────────────────────

export class TrustService {
  // Upsert a new pubkey with default values.
  async getOrCreate(pubkey: string): Promise<NostrIdentity> {
    const existing = await this.getIdentity(pubkey);
    if (existing) return existing;

    const rows = await db
      .insert(nostrIdentities)
      .values({ pubkey })
      .onConflictDoNothing()
      .returning();

    const row = rows[0];
    if (row) return row;

    // Race: another request inserted first
    return (await this.getIdentity(pubkey))!;
  }

  async getIdentity(pubkey: string): Promise<NostrIdentity | null> {
    const rows = await db
      .select()
      .from(nostrIdentities)
      .where(eq(nostrIdentities.pubkey, pubkey))
      .limit(1);
    return rows[0] ?? null;
  }

  // Returns the trust tier for a pubkey, or "new" if unknown.
  async getTier(pubkey: string): Promise<TrustTier> {
    const identity = await this.getIdentity(pubkey);
    if (!identity) return "new";
    const decayedScore = applyDecay(identity);
    return computeTier(decayedScore);
  }

  // Returns full identity row with decayed score applied (does NOT persist decay).
  async getIdentityWithDecay(pubkey: string): Promise<(NostrIdentity & { tier: TrustTier }) | null> {
    const identity = await this.getIdentity(pubkey);
    if (!identity) return null;
    const score = applyDecay(identity);
    const tier = computeTier(score);
    return { ...identity, trustScore: score, tier };
  }

  // Called after a successful (paid) interaction.
  // Decay is applied first so long-absent identities start from their decayed
  // baseline rather than the raw stored score.
  async recordSuccess(pubkey: string, satsCost: number): Promise<void> {
    const identity = await this.getOrCreate(pubkey);
    const decayedBase = applyDecay(identity);
    const newScore = decayedBase + SCORE_PER_SUCCESS;
    const newTier = computeTier(newScore);

    await db
      .update(nostrIdentities)
      .set({
        trustScore: newScore,
        tier: newTier,
        interactionCount: identity.interactionCount + 1,
        lastSeen: new Date(),
        updatedAt: new Date(),
      })
      .where(eq(nostrIdentities.pubkey, pubkey));

    logger.info("trust: success recorded", {
      pubkey: pubkey.slice(0, 8),
      decayedBase,
      newScore,
      newTier,
      satsCost,
    });

    // Sync relay access whenever the tier may have changed
    relayAccountService.syncFromTrustTier(pubkey).catch((err) =>
      logger.warn("relay sync failed after success", { pubkey: pubkey.slice(0, 8), err }),
    );
  }

  // Called after a failed, rejected, or abusive interaction.
  // Decay is applied first so mutations always start from the current true baseline.
  async recordFailure(pubkey: string, reason: string): Promise<void> {
    const identity = await this.getOrCreate(pubkey);
    const decayedBase = applyDecay(identity);
    const newScore = Math.max(0, decayedBase - SCORE_PER_FAILURE);
    const newTier = computeTier(newScore);

    await db
      .update(nostrIdentities)
      .set({
        trustScore: newScore,
        tier: newTier,
        interactionCount: identity.interactionCount + 1,
        lastSeen: new Date(),
        updatedAt: new Date(),
      })
      .where(eq(nostrIdentities.pubkey, pubkey));

    logger.info("trust: failure recorded", {
      pubkey: pubkey.slice(0, 8),
      decayedBase,
      newScore,
      newTier,
      reason,
    });

    // Sync relay access on tier change (may revoke write on repeated failures)
    relayAccountService.syncFromTrustTier(pubkey).catch((err) =>
      logger.warn("relay sync failed after failure", { pubkey: pubkey.slice(0, 8), err }),
    );
  }

  // Issue a signed identity token for a verified pubkey.
  issueToken(pubkey: string): string {
    return issueToken(pubkey);
  }

  // Verify and parse an X-Nostr-Token header value.
  verifyToken(token: string): { pubkey: string; expiry: number } | null {
    return verifyToken(token);
  }
}

export const trustService = new TrustService();
