/**
 * nostr-identity.js — Browser-side Nostr identity for Timmy Tower World.
 *
 * Supports two identity modes:
 *   1. NIP-07 extension (window.nostr) — preferred when available.
 *   2. Generated keypair stored in localStorage — fallback for users without
 *      a Nostr extension. The user is shown an opt-in prompt before a key
 *      is generated; keys are never generated silently.
 *
 * Public API:
 *   initNostrIdentity(apiBase)   → Promise<void>  — call once on page load.
 *   getNostrToken()              → string|null    — current signed token.
 *   getPubkey()                  → string|null    — hex pubkey.
 *   getOrRefreshToken(apiBase)   → Promise<string|null> — valid token or refresh.
 *   hasIdentity()                → boolean
 *   showIdentityPrompt()         → void — display the "Identify with Nostr" UI
 */

import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { hexToBytes, bytesToHex } from '@noble/hashes/utils.js';
import { decode as nip19Decode } from 'nostr-tools/nip19';

const LS_KEYPAIR_KEY = 'timmy_nostr_keypair_v1';
const LS_TOKEN_KEY   = 'timmy_nostr_token_v1';

// Token lifetime: 23 h — server TTL is 24 h; refresh 1 h before expiry
const TOKEN_TTL_MS = 23 * 3600 * 1000;

let _pubkey    = null;  // hex
let _token     = null;  // nostr_token string from server
let _tokenExp  = 0;     // unix ms when token was fetched
let _useNip07  = false; // true if window.nostr is available
let _canSign   = false; // true if we have a signing capability (NIP-07 or local privkey)
let _identityPromptShown = false;

// ── Init ──────────────────────────────────────────────────────────────────────

export async function initNostrIdentity(apiBase = '/api') {
  // Prefer NIP-07 extension
  if (typeof window !== 'undefined' && window.nostr) {
    try {
      _pubkey   = await window.nostr.getPublicKey();
      _useNip07 = true;
      _canSign  = true;
      console.info('[nostr] Using NIP-07 extension, pubkey:', _pubkey.slice(0, 8) + '…');
    } catch (err) {
      console.warn('[nostr] NIP-07 getPublicKey failed, will use local keypair', err);
      _useNip07 = false;
    }
  }

  // Try restoring / discovering an existing keypair
  if (!_pubkey) {
    _pubkey = _loadKeypair();
    if (_pubkey) {
      // Check if we actually have a privkey for signing (npub-only discovery gives pubkey but no privkey)
      _canSign = !!_getPrivkeyBytes();
      if (_canSign) {
        console.info('[nostr] Restored local keypair (with signing), pubkey:', _pubkey.slice(0, 8) + '…');
      } else {
        console.info('[nostr] Discovered pubkey (view-only, no privkey), pubkey:', _pubkey.slice(0, 8) + '…');
      }
    }
  }

  // Try to restore cached token
  _loadCachedToken();

  // If we have signing capability and no valid token, authenticate now
  if (_pubkey && _canSign && !_isTokenValid()) {
    await refreshToken(apiBase);
  }

  // Show the opt-in prompt if:
  //   a) No identity at all — user can generate a keypair or connect NIP-07
  //   b) Have a pubkey but no signing capability (npub-only discovery) —
  //      user should be offered a way to add signing (generate new key or NIP-07)
  if (!_pubkey || !_canSign) {
    _scheduleIdentityPrompt(apiBase);
  }
}

// ── Public API ────────────────────────────────────────────────────────────────

export function getPubkey() { return _pubkey; }
export function getNostrToken() { return _isTokenValid() ? _token : null; }
export function hasIdentity() { return !!_pubkey; }

/**
 * getOrRefreshToken — returns a valid token, refreshing if necessary.
 * Returns null if no identity is established.
 */
export async function getOrRefreshToken(apiBase = '/api') {
  if (!_pubkey) return null;
  if (_isTokenValid()) return _token;
  return refreshToken(apiBase);
}

/**
 * refreshToken — run the challenge→sign→verify flow with the API.
 *
 * Server API:
 *   POST /api/identity/challenge  → { nonce, expiresAt }
 *   POST /api/identity/verify     → { nostr_token, pubkey, trust }
 *     body: { event }  where event.content = nonce, event.kind = 27235
 */
export async function refreshToken(apiBase = '/api') {
  if (!_pubkey) return null;

  try {
    // 1. POST /api/identity/challenge — server returns a nonce
    const challengeRes = await fetch(`${apiBase}/identity/challenge`, { method: 'POST' });
    if (!challengeRes.ok) {
      console.warn('[nostr] challenge fetch failed', challengeRes.status);
      return null;
    }
    const { nonce } = await challengeRes.json();
    if (!nonce) { console.warn('[nostr] no nonce in challenge response'); return null; }

    // 2. Sign the nonce as event.content (kind 27235 per NIP-98)
    const event = await _signNonce(nonce);
    if (!event) { console.warn('[nostr] signing failed'); return null; }

    // 3. POST /api/identity/verify — server checks sig and issues nostr_token
    const verifyRes = await fetch(`${apiBase}/identity/verify`, {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify({ event }),
    });
    if (!verifyRes.ok) {
      console.warn('[nostr] verify failed', verifyRes.status);
      return null;
    }
    const data = await verifyRes.json();
    const token = data.nostr_token;
    if (!token) { console.warn('[nostr] no nostr_token in verify response'); return null; }

    _token    = token;
    _tokenExp = Date.now();
    _saveCachedToken();
    console.info('[nostr] token refreshed, tier:', data.trust?.tier ?? 'unknown');
    return token;
  } catch (err) {
    console.warn('[nostr] refreshToken error', err);
    return null;
  }
}

// ── Identity prompt — optional Nostr identification UI ────────────────────────
// Shown after a short delay if no identity is present. User can dismiss.

export function showIdentityPrompt(apiBase = '/api') {
  if (_identityPromptShown || _pubkey) return;
  _identityPromptShown = true;

  const $prompt = document.createElement('div');
  $prompt.id = 'nostr-identity-prompt';
  $prompt.style.cssText = [
    'position:fixed;bottom:80px;right:16px;z-index:900',
    'background:rgba(8,6,16,0.94);border:1px solid #3355aa',
    'border-radius:8px;padding:12px 14px;font-family:monospace',
    'font-size:12px;color:#88aadd;max-width:240px',
    'box-shadow:0 0 16px rgba(50,80,200,0.3)',
    'animation:fadeInUp .3s ease',
  ].join(';');

  const hasNip07 = typeof window !== 'undefined' && !!window.nostr;
  $prompt.innerHTML = `
    <div style="color:#aaccff;font-weight:bold;margin-bottom:6px">
      ⚡ Identify with Nostr
    </div>
    <div style="line-height:1.5;margin-bottom:10px">
      ${hasNip07
        ? 'Connect your Nostr extension to unlock trust benefits and free-tier access.'
        : 'Generate a local Nostr identity to unlock trust benefits and free-tier access.'}
    </div>
    <div style="display:flex;gap:8px">
      <button id="nostr-prompt-connect"
        style="flex:1;background:#1a3a7a;color:#aaddff;border:1px solid #3366bb;
               border-radius:4px;padding:5px 8px;cursor:pointer;font-family:monospace;font-size:11px">
        ${hasNip07 ? 'Connect Extension' : 'Generate Key'}
      </button>
      <button id="nostr-prompt-dismiss"
        style="background:transparent;color:#556688;border:none;cursor:pointer;font-size:11px">
        ✕
      </button>
    </div>
  `;

  document.body.appendChild($prompt);

  document.getElementById('nostr-prompt-connect')?.addEventListener('click', async () => {
    $prompt.remove();
    if (hasNip07) {
      // NIP-07 connect
      try {
        _pubkey   = await window.nostr.getPublicKey();
        _useNip07 = true;
        _canSign  = true;
      } catch { return; }
    } else {
      // Generate + store keypair (user consented by clicking)
      _pubkey  = _generateAndSaveKeypair();
      _canSign = true;
    }
    if (_pubkey && _canSign) {
      await refreshToken(apiBase);
      _updateIdentityHUD();
    }
  });

  document.getElementById('nostr-prompt-dismiss')?.addEventListener('click', () => {
    $prompt.remove();
  });
}

function _scheduleIdentityPrompt(apiBase) {
  // Show prompt 4 s after page load — after Three.js world is running
  setTimeout(() => showIdentityPrompt(apiBase), 4000);
}

function _updateIdentityHUD() {
  // Notify the session button / HUD area if session.js is connected.
  // We dispatch a custom event so session.js can pick it up without a circular dep.
  window.dispatchEvent(new CustomEvent('nostr:identity-ready', { detail: { pubkey: _pubkey } }));
}

// ── Keypair helpers ───────────────────────────────────────────────────────────

/**
 * _scanExistingNostrKeys — looks for pre-existing Nostr keys in localStorage
 * using common patterns used by Nostr clients:
 *   - Our own LS_KEYPAIR_KEY format { pubkey, privkey }
 *   - npub1... / nsec1... bech32 strings stored under common key names
 *   - Raw 64-char hex pubkeys / privkeys
 *
 * Returns { pubkey: hex, privkey: hex|null } or null if nothing found.
 * Does NOT generate new keys — only discovers existing ones.
 */
function _scanExistingNostrKeys() {
  // 1. Our own format first
  try {
    const stored = localStorage.getItem(LS_KEYPAIR_KEY);
    if (stored) {
      const parsed = JSON.parse(stored);
      if (parsed?.pubkey) return { pubkey: parsed.pubkey, privkey: parsed.privkey ?? null };
    }
  } catch { /* corrupt */ }

  // 2. Common key names used by Nostr clients (nos2x-style, nostrid, Alby export, etc.)
  const COMMON_LS_KEYS = [
    'nostr_privkey', 'privkey', 'nsec', 'npub',
    'nostr-privkey', 'nostr-nsec', 'nostr_nsec',
    'nostr_keys', 'nostrKeys', 'nostr:privkey',
    // nos2x stores its key here:
    'nostr-extension:privkey',
  ];

  for (const key of COMMON_LS_KEYS) {
    try {
      const val = localStorage.getItem(key);
      if (!val) continue;

      // Try bech32 nsec1...
      if (val.startsWith('nsec1')) {
        try {
          const decoded = nip19Decode(val);
          if (decoded.type === 'nsec') {
            const privBytes = decoded.data;
            const pubkeyHex = getPublicKey(privBytes);
            const privkeyHex = bytesToHex(privBytes);
            // Save in our format for subsequent loads
            _saveDiscoveredKeypair(pubkeyHex, privkeyHex);
            console.info(`[nostr] Discovered nsec key at localStorage['${key}']`);
            return { pubkey: pubkeyHex, privkey: privkeyHex };
          }
        } catch { /* invalid bech32 */ }
      }

      // Try bech32 npub1... (pubkey only — no signing possible without privkey)
      if (val.startsWith('npub1')) {
        try {
          const decoded = nip19Decode(val);
          if (decoded.type === 'npub') {
            console.info(`[nostr] Discovered npub key at localStorage['${key}'] (view-only)`);
            return { pubkey: decoded.data, privkey: null };
          }
        } catch { /* invalid */ }
      }

      // Try raw hex privkey (64 chars)
      if (/^[0-9a-f]{64}$/.test(val)) {
        try {
          const privBytes = hexToBytes(val);
          const pubkeyHex = getPublicKey(privBytes);
          _saveDiscoveredKeypair(pubkeyHex, val);
          console.info(`[nostr] Discovered hex privkey at localStorage['${key}']`);
          return { pubkey: pubkeyHex, privkey: val };
        } catch { /* bad key */ }
      }

      // Try JSON objects with common shapes
      if (val.startsWith('{')) {
        try {
          const obj = JSON.parse(val);
          const nsec = obj.nsec || obj.privkey || obj.private_key || obj.secret;
          const npub = obj.npub || obj.pubkey || obj.public_key;
          if (nsec && nsec.startsWith('nsec1')) {
            const decoded = nip19Decode(nsec);
            if (decoded.type === 'nsec') {
              const privBytes = decoded.data;
              const pubkeyHex = getPublicKey(privBytes);
              const privkeyHex = bytesToHex(privBytes);
              _saveDiscoveredKeypair(pubkeyHex, privkeyHex);
              console.info(`[nostr] Discovered nsec in JSON at localStorage['${key}']`);
              return { pubkey: pubkeyHex, privkey: privkeyHex };
            }
          }
          if (npub && npub.startsWith('npub1')) {
            const decoded = nip19Decode(npub);
            if (decoded.type === 'npub') {
              return { pubkey: decoded.data, privkey: null };
            }
          }
          if (typeof obj.pubkey === 'string' && /^[0-9a-f]{64}$/.test(obj.pubkey)) {
            const pkey = obj.privkey ?? obj.private_key ?? null;
            return { pubkey: obj.pubkey, privkey: pkey };
          }
        } catch { /* bad JSON */ }
      }
    } catch { /* localStorage access failed */ }
  }

  return null;
}

function _saveDiscoveredKeypair(pubkeyHex, privkeyHex) {
  try {
    localStorage.setItem(LS_KEYPAIR_KEY, JSON.stringify({ pubkey: pubkeyHex, privkey: privkeyHex }));
  } catch { /* storage full */ }
}

function _loadKeypair() {
  const found = _scanExistingNostrKeys();
  if (found) {
    // If only npub found (no privkey), store pubkey but signing won't work
    // (the prompt will offer extension or key generation for signing)
    return found.pubkey;
  }
  return null;
}

function _generateAndSaveKeypair() {
  const privkeyBytes = generateSecretKey();
  const privkeyHex   = bytesToHex(privkeyBytes);
  const pubkeyHex    = getPublicKey(privkeyBytes);

  try {
    localStorage.setItem(LS_KEYPAIR_KEY, JSON.stringify({ privkey: privkeyHex, pubkey: pubkeyHex }));
  } catch { /* storage full or private browsing */ }

  return pubkeyHex;
}

function _getPrivkeyBytes() {
  try {
    const stored = localStorage.getItem(LS_KEYPAIR_KEY);
    if (!stored) return null;
    const { privkey } = JSON.parse(stored);
    return privkey ? hexToBytes(privkey) : null;
  } catch {
    return null;
  }
}

// ── Signing ───────────────────────────────────────────────────────────────────

async function _signNonce(nonce) {
  // event.content must be the nonce string per server contract
  const eventTemplate = {
    kind:       27235,
    created_at: Math.floor(Date.now() / 1000),
    tags:       [],
    content:    nonce,
  };

  if (_useNip07 && window.nostr) {
    try {
      return await window.nostr.signEvent(eventTemplate);
    } catch (err) {
      console.warn('[nostr] NIP-07 signEvent failed, trying local key', err);
      _useNip07 = false;
    }
  }

  const privkeyBytes = _getPrivkeyBytes();
  if (!privkeyBytes) {
    console.warn('[nostr] no private key available for signing');
    return null;
  }

  try {
    return finalizeEvent({ ...eventTemplate, pubkey: _pubkey }, privkeyBytes);
  } catch (err) {
    console.warn('[nostr] finalizeEvent failed', err);
    return null;
  }
}

// ── Token caching ─────────────────────────────────────────────────────────────

function _isTokenValid() {
  if (!_token || !_tokenExp) return false;
  return (Date.now() - _tokenExp) < TOKEN_TTL_MS;
}

function _saveCachedToken() {
  try {
    localStorage.setItem(LS_TOKEN_KEY, JSON.stringify({ token: _token, exp: _tokenExp }));
  } catch { /* unavailable */ }
}

function _loadCachedToken() {
  try {
    const raw = localStorage.getItem(LS_TOKEN_KEY);
    if (!raw) return;
    const { token, exp } = JSON.parse(raw);
    if (token && exp) { _token = token; _tokenExp = exp; }
  } catch { /* ignore */ }
}
