/**
 * relay-accounts.ts — Relay account whitelist + access management.
 *
 * Trust tier → access level defaults (env-overridable):
 *   new         → read  (RELAY_ACCESS_NEW,         default "read")
 *   established → write (RELAY_ACCESS_ESTABLISHED,  default "write")
 *   trusted     → write (RELAY_ACCESS_TRUSTED,      default "write")
 *   elite       → write (RELAY_ACCESS_ELITE,        default "write")
 *
 * Access semantics:
 *   "write" → relay policy returns "accept" (active write access)
 *   "read"  → relay policy returns "reject" (read-only; no write permitted)
 *   "none"  → relay policy returns "reject" (default deny; no access)
 *
 * Revocation — grantedBy sentinel "manual-revoked":
 *   The base contract for grantedBy is "manual" | "auto-tier".
 *   revoke() additionally writes "manual-revoked" as a sentinel to prevent
 *   syncFromTrustTier() from auto-reinstating the account on the next trust
 *   update. This is intentional: admin revocation must be permanent until
 *   an explicit grant() call restores access. The sentinel is an internal
 *   implementation detail; callers should treat "manual-revoked" rows as
 *   revoked and must not try to grant access by reusing the sentinel value.
 */

import { db, nostrIdentities, relayAccounts } from "@workspace/db";
import type { RelayAccessLevel, TrustTier } from "@workspace/db";
import { eq } from "drizzle-orm";
import { makeLogger } from "./logger.js";

const logger = makeLogger("relay-accounts");

// ── Tier → access level mapping ───────────────────────────────────────────────

function envAccess(name: string, fallback: RelayAccessLevel): RelayAccessLevel {
  const v = process.env[name]?.toLowerCase();
  if (v === "write" || v === "read" || v === "none") return v;
  return fallback;
}

const TIER_ACCESS: Record<TrustTier, RelayAccessLevel> = {
  new:         envAccess("RELAY_ACCESS_NEW",         "read"),
  established: envAccess("RELAY_ACCESS_ESTABLISHED",  "write"),
  trusted:     envAccess("RELAY_ACCESS_TRUSTED",      "write"),
  elite:       envAccess("RELAY_ACCESS_ELITE",        "write"),
};

const ACCESS_RANK: Record<RelayAccessLevel, number> = { none: 0, read: 1, write: 2 };

// ── RelayAccountService ───────────────────────────────────────────────────────

export class RelayAccountService {
  /**
   * Returns the current access level for a pubkey.
   * If the pubkey has no relay_accounts row, OR has a revoked row → "none".
   */
  async getAccess(pubkey: string): Promise<RelayAccessLevel> {
    const rows = await db
      .select({ accessLevel: relayAccounts.accessLevel, revokedAt: relayAccounts.revokedAt })
      .from(relayAccounts)
      .where(eq(relayAccounts.pubkey, pubkey))
      .limit(1);

    const row = rows[0];
    if (!row || row.revokedAt !== null) return "none";
    return row.accessLevel;
  }

  /**
   * Grant (or upgrade) relay access for a pubkey.
   * If the pubkey does not exist in nostr_identities it is upserted first
   * so the FK constraint is satisfied.
   */
  async grant(
    pubkey: string,
    level: RelayAccessLevel,
    reason: string,
    grantedBy: "manual" | "auto-tier" = "manual",
  ): Promise<void> {
    // Ensure FK target exists
    await db
      .insert(nostrIdentities)
      .values({ pubkey })
      .onConflictDoNothing();

    await db
      .insert(relayAccounts)
      .values({
        pubkey,
        accessLevel: level,
        grantedBy,
        grantedAt: new Date(),
        revokedAt: null,
        notes: reason,
      })
      .onConflictDoUpdate({
        target: relayAccounts.pubkey,
        set: {
          accessLevel: level,
          grantedBy,
          grantedAt: new Date(),
          revokedAt: null,
          notes: reason,
        },
      });

    logger.info("relay access granted", {
      pubkey: pubkey.slice(0, 8),
      level,
      grantedBy,
      reason,
    });
  }

  /**
   * Revoke relay access for a pubkey.
   * Sets revokedAt, access_level → "none", grantedBy → "manual-revoked".
   * The "manual-revoked" marker prevents syncFromTrustTier() from auto-reinstating
   * this account on the next trust update. Only an explicit grant() by an admin
   * can restore access after revocation.
   */
  async revoke(pubkey: string, reason?: string): Promise<void> {
    await db
      .update(relayAccounts)
      .set({
        accessLevel: "none",
        grantedBy: "manual-revoked",
        revokedAt: new Date(),
        notes: reason ?? null,
      })
      .where(eq(relayAccounts.pubkey, pubkey));

    logger.info("relay access revoked", {
      pubkey: pubkey.slice(0, 8),
      reason,
    });
  }

  /**
   * Seed a pubkey with elite identity state and relay write access.
   * Called at startup for Timmy's own pubkey. Idempotent.
   *
   * Sets nostr_identities.tier = "elite" to reflect the elite trust state,
   * then grants relay write access as a manual (permanent) grant.
   */
  async seedElite(pubkey: string, notes: string): Promise<void> {
    const ELITE_SCORE = 200; // matches TIER_ELITE threshold default

    // Upsert nostr_identities with elite tier + score
    await db
      .insert(nostrIdentities)
      .values({
        pubkey,
        tier: "elite",
        trustScore: ELITE_SCORE,
        lastSeen: new Date(),
      })
      .onConflictDoUpdate({
        target: nostrIdentities.pubkey,
        set: {
          tier: "elite",
          trustScore: ELITE_SCORE,
          updatedAt: new Date(),
        },
      });

    // Grant relay write access (manual — never overridden by auto-tier sync)
    await this.grant(pubkey, "write", notes, "manual");

    logger.info("relay: elite seed applied", {
      pubkey: pubkey.slice(0, 8),
      notes,
    });
  }

  /**
   * Sync relay access from the current trust tier.
   * Fetches the pubkey's tier from nostr_identities internally.
   *
   * Rules:
   * - "manual-revoked" rows: never auto-reinstated
   * - "manual" rows (active): only upgraded, never downgraded
   * - "auto-tier" rows: always synced to current tier's target access level
   * - No existing row: create one if the tier earns access (non-"none")
   */
  async syncFromTrustTier(pubkey: string): Promise<void> {
    // Fetch current tier from DB (written by TrustService before this is called)
    const identityRows = await db
      .select({ tier: nostrIdentities.tier })
      .from(nostrIdentities)
      .where(eq(nostrIdentities.pubkey, pubkey))
      .limit(1);

    const tier: TrustTier = (identityRows[0]?.tier as TrustTier) ?? "new";
    const targetLevel = TIER_ACCESS[tier];

    const rows = await db
      .select()
      .from(relayAccounts)
      .where(eq(relayAccounts.pubkey, pubkey))
      .limit(1);

    const existing = rows[0];

    if (!existing) {
      if (targetLevel !== "none") {
        await this.grant(pubkey, targetLevel, `auto: tier promotion to ${tier}`, "auto-tier");
      }
      return;
    }

    // Manually revoked — admin must explicitly re-grant; never auto-reinstate
    if (existing.grantedBy === "manual-revoked") {
      return;
    }

    // Active manual grant — only upgrade, never downgrade
    if (existing.grantedBy === "manual" && existing.revokedAt === null) {
      if (ACCESS_RANK[targetLevel] > ACCESS_RANK[existing.accessLevel]) {
        await this.grant(pubkey, targetLevel, `auto-upgrade: tier ${tier}`, "manual");
      }
      return;
    }

    // Auto-tier row — sync to current tier's target level
    if (targetLevel !== existing.accessLevel || existing.revokedAt !== null) {
      await this.grant(pubkey, targetLevel, `auto: tier ${tier} → ${targetLevel}`, "auto-tier");
    }
  }

  /**
   * List all relay accounts (for admin UI).
   * Optionally filter to active (non-revoked) rows only.
   */
  async list(opts: { activeOnly?: boolean } = {}): Promise<typeof relayAccounts.$inferSelect[]> {
    const rows = await db.select().from(relayAccounts).orderBy(relayAccounts.grantedAt);
    if (opts.activeOnly) {
      return rows.filter((r) => r.revokedAt === null && r.accessLevel !== "none");
    }
    return rows;
  }
}

export const relayAccountService = new RelayAccountService();
