import { Hono } from 'hono';
import type { CryptoProvider } from '@shade/core';
import { errorToHttpStatus, ShadeError, ValidationError, RateLimitError } from '@shade/core';
import type { PrekeyStore } from './store.js';
import { verifyPayload, validateAddress } from './auth.js';
import { RateLimiter, MemoryRateLimitStore, REGISTER_LIMIT, FETCH_LIMIT, REPLENISH_LIMIT, DELETE_LIMIT } from './rate-limit.js';
import { PrekeyServerEvents, shortHash } from './events.js';
import {
  ATTR_ERROR_CODE,
  ATTR_HTTP_STATUS,
  ATTR_ROUTE,
  NOOP_HOOK,
  type ObservabilityHook,
} from '@shade/observability';
import type { KeyTransparencyService } from './kt-integration.js';
import { encodeProofForWire, encodeSthForWire } from './kt-integration.js';
import { createKTRoutes } from './kt-routes.js';

/** Max POST body size in bytes (64KB) */
const MAX_BODY_SIZE = 64 * 1024;

/**
 * Create the Shade Prekey Server Hono app.
 *
 * Routes:
 *   POST   /v1/keys/register        — Register identity + upload prekey bundle (SIGNED)
 *   GET    /v1/keys/bundle/:address  — Fetch a prekey bundle (anonymous)
 *   POST   /v1/keys/replenish       — Upload additional one-time prekeys (SIGNED)
 *   GET    /v1/keys/count/:address   — Get remaining one-time prekey count (anonymous)
 *   DELETE /v1/keys/:address         — Unregister (SIGNED)
 *
 * Write routes require a valid Ed25519 signature. See auth.ts.
 */
export interface PrekeyRoutesOptions {
  /** Disable rate limiting (for tests). Default: enabled. */
  disableRateLimit?: boolean;
  /** Optional event emitter for observability. */
  events?: PrekeyServerEvents;
  /**
   * Optional OTel observability hook. When supplied (and the runtime gate
   * is on), each request gets a `shade.prekey.<route>` span with route
   * + HTTP-status attributes. PII-safe: never logs the address path
   * parameter or client IP.
   */
  observability?: ObservabilityHook;
  /**
   * Optional Key-Transparency service (V3.12). When provided:
   *   - Every `register` and `delete` mutation is committed to the log.
   *   - `GET /v1/keys/bundle/:address` includes a `ktProof` field.
   *   - `/v1/kt/*` routes are mounted (latest STH, historical STHs,
   *     consistency proofs, log_id pinning info).
   * When absent, the server is byte-compatible with pre-V3.12 clients.
   */
  keyTransparency?: KeyTransparencyService;
}

export function createPrekeyRoutes(
  store: PrekeyStore,
  crypto: CryptoProvider,
  options: PrekeyRoutesOptions = {},
): Hono {
  const app = new Hono();
  const events = options.events;
  const observability = options.observability ?? NOOP_HOOK;
  const kt = options.keyTransparency;

  // Per-request span middleware — runs first so it covers handlers AND
  // the global error handler. Span name is the route template (e.g.
  // `/v1/keys/bundle/:address`), so cardinality stays bounded and the
  // address itself never enters span data.
  app.use('*', async (c, next) => {
    const route = c.req.routePath ?? c.req.path ?? '<unknown>';
    const span = observability.startSpan('shade.prekey.request', {
      [ATTR_ROUTE]: route,
    });
    try {
      await next();
      span.setAttribute(ATTR_HTTP_STATUS, c.res.status);
      span.setStatus(c.res.status >= 500 ? 'error' : 'ok');
    } catch (err) {
      const code =
        err instanceof ShadeError
          ? err.code ?? 'SHADE_ERROR'
          : 'SHADE_INTERNAL';
      span.setAttribute(ATTR_ERROR_CODE, code);
      span.recordException(err);
      span.setStatus('error', code);
      throw err;
    } finally {
      span.end();
    }
  });

  // Rate limiters (one per route, per IP or per identity)
  const rlStore = new MemoryRateLimitStore();
  const registerRL = new RateLimiter(rlStore, REGISTER_LIMIT);
  const fetchRL = new RateLimiter(rlStore, FETCH_LIMIT);
  const replenishRL = new RateLimiter(rlStore, REPLENISH_LIMIT);
  const deleteRL = new RateLimiter(rlStore, DELETE_LIMIT);
  const rateLimitEnabled = !options.disableRateLimit;

  // Helper: extract client IP from request headers
  const getClientIp = (c: any): string => {
    return (
      c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ??
      c.req.header('x-real-ip') ??
      'unknown'
    );
  };

  // Lightweight health endpoint (always available, no auth)
  app.get('/health', (c) => c.json({ status: 'ok', service: 'shade-prekey-server' }));

  // Global error handler — maps ShadeError to HTTP status
  app.onError((err, c) => {
    if (err instanceof RateLimitError) {
      // Emit rate-limited event before responding
      events?.emit('server.rate_limited', {
        route: c.req.routePath ?? c.req.path,
        key: getClientIp(c),
      });
    }
    if (err instanceof ShadeError) {
      const status = errorToHttpStatus(err);
      const body: any = err.toJSON();
      if ((err as any).retryAfterSeconds) {
        c.header('Retry-After', String((err as any).retryAfterSeconds));
      }
      return c.json(body, status as any);
    }
    console.error('[Shade] Unhandled error:', err);
    return c.json({ error: 'Internal server error' }, 500);
  });

  // ─── Register ──────────────────────────────────────────────
  app.post('/v1/keys/register', async (c) => {
    if (rateLimitEnabled) await registerRL.consume(`register:${getClientIp(c)}`);

    const rawBody = await c.req.text();
    if (rawBody.length > MAX_BODY_SIZE) {
      throw new ValidationError(`Request body too large (max ${MAX_BODY_SIZE} bytes)`);
    }
    const body = JSON.parse(rawBody);
    const { address, identitySigningKey, identityDHKey, signedPreKey, oneTimePreKeys } = body;

    const addr = validateAddress(address);
    if (!identitySigningKey || !identityDHKey || !signedPreKey) {
      throw new ValidationError('Missing required fields');
    }

    const signingKey = b64ToBytes(identitySigningKey);
    const dhKey = b64ToBytes(identityDHKey);

    // Verify signature against the identity's own signing key (TOFU)
    await verifyPayload(crypto, signingKey, body);

    await store.saveIdentity(addr, signingKey, dhKey);
    await store.saveSignedPreKey(
      addr,
      signedPreKey.keyId,
      b64ToBytes(signedPreKey.publicKey),
      b64ToBytes(signedPreKey.signature),
    );

    if (oneTimePreKeys && Array.isArray(oneTimePreKeys)) {
      const keys = oneTimePreKeys.map((k: any) => ({
        keyId: k.keyId,
        publicKey: b64ToBytes(k.publicKey),
      }));
      await store.saveOneTimePreKeys(addr, keys);
    }

    // Commit to KT log (if enabled). The bundle covered by the commitment
    // is { signing_key, dh_key, signed_prekey } — one-time prekeys are
    // intentionally excluded so OTP rotation doesn't churn the log.
    if (kt) {
      await kt.recordRegister(addr, {
        identitySigningKey: signingKey,
        identityDHKey: dhKey,
        signedPreKey: {
          keyId: signedPreKey.keyId,
          publicKey: b64ToBytes(signedPreKey.publicKey),
          signature: b64ToBytes(signedPreKey.signature),
        },
      });
    }

    if (events) {
      const hash = await shortHash(signingKey);
      events.emit('server.identity_registered', { address: addr, identityKeyHash: hash });
    }

    return c.json({ ok: true });
  });

  // ─── Fetch Bundle (anonymous) ──────────────────────────────
  app.get('/v1/keys/bundle/:address', async (c) => {
    if (rateLimitEnabled) await fetchRL.consume(`fetch:${getClientIp(c)}`);
    const address = validateAddress(c.req.param('address'));

    const identity = await store.getIdentity(address);
    if (!identity) {
      // KT-enabled: pin the negative answer to a tree state. If the
      // address has been tombstoned we serve the tombstone (inclusion)
      // proof; otherwise an absence proof.
      if (kt) {
        const inclusion = await kt.buildBundleInclusion(address);
        if (inclusion) {
          return c.json(
            {
              error: 'Address not found',
              code: 'SHADE_NOT_FOUND',
              ktProof: encodeProofForWire(inclusion),
            },
            404,
          );
        }
        const absence = await kt.buildAbsence(address);
        if (absence) {
          return c.json(
            {
              error: 'Address not found',
              code: 'SHADE_NOT_FOUND',
              ktProof: encodeProofForWire(absence),
            },
            404,
          );
        }
      }
      return c.json({ error: 'Address not found', code: 'SHADE_NOT_FOUND' }, 404);
    }

    const signedPreKey = await store.getSignedPreKey(address);
    if (!signedPreKey) {
      return c.json({ error: 'No signed prekey', code: 'SHADE_NOT_FOUND' }, 404);
    }

    const oneTimePreKey = await store.consumeOneTimePreKey(address);

    const bundle: any = {
      identitySigningKey: bytesToB64(identity.identitySigningKey),
      identityDHKey: bytesToB64(identity.identityDHKey),
      signedPreKey: {
        keyId: signedPreKey.keyId,
        publicKey: bytesToB64(signedPreKey.publicKey),
        signature: bytesToB64(signedPreKey.signature),
      },
    };

    if (oneTimePreKey) {
      bundle.oneTimePreKey = {
        keyId: oneTimePreKey.keyId,
        publicKey: bytesToB64(oneTimePreKey.publicKey),
      };
    }

    if (kt) {
      const proof = await kt.buildBundleInclusion(address);
      if (proof) {
        bundle.ktProof = encodeProofForWire(proof);
      } else {
        // No live entry in the index — fall back to STH so client at
        // least sees a fresh tree-head, then surfaces "no proof available"
        // as a soft warning.
        const sth = await kt.getLatestSTH();
        bundle.ktSth = encodeSthForWire(sth);
      }
    }

    // Update activity so stale cleanup doesn't purge active addresses
    await store.touchIdentity(address);

    events?.emit('server.bundle_fetched', {
      address,
      hadOneTimePreKey: oneTimePreKey != null,
    });

    return c.json(bundle);
  });

  // ─── Replenish One-Time Prekeys (signed) ───────────────────
  app.post('/v1/keys/replenish', async (c) => {
    const rawBody = await c.req.text();
    if (rawBody.length > MAX_BODY_SIZE) {
      throw new ValidationError(`Request body too large (max ${MAX_BODY_SIZE} bytes)`);
    }
    const body = JSON.parse(rawBody);
    const { address, oneTimePreKeys } = body;

    const addr = validateAddress(address);
    if (rateLimitEnabled) await replenishRL.consume(`replenish:${addr}`);
    if (!oneTimePreKeys || !Array.isArray(oneTimePreKeys)) {
      throw new ValidationError('Missing oneTimePreKeys');
    }

    // Look up the stored identity and verify the signature against it
    const identity = await store.getIdentity(addr);
    if (!identity) {
      return c.json({ error: 'Address not registered', code: 'SHADE_NOT_FOUND' }, 404);
    }
    await verifyPayload(crypto, identity.identitySigningKey, body);

    const keys = oneTimePreKeys.map((k: any) => ({
      keyId: k.keyId,
      publicKey: b64ToBytes(k.publicKey),
    }));
    await store.saveOneTimePreKeys(addr, keys);
    await store.touchIdentity(addr);

    const count = await store.getOneTimePreKeyCount(addr);
    events?.emit('server.prekeys_replenished', {
      address: addr,
      count: keys.length,
      totalAfter: count,
    });
    return c.json({ ok: true, remaining: count });
  });

  // ─── Get Count (anonymous) ─────────────────────────────────
  app.get('/v1/keys/count/:address', async (c) => {
    const address = validateAddress(c.req.param('address'));
    const count = await store.getOneTimePreKeyCount(address);
    return c.json({ count });
  });

  // ─── Delete (signed) ───────────────────────────────────────
  app.delete('/v1/keys/:address', async (c) => {
    const address = validateAddress(c.req.param('address'));
    if (rateLimitEnabled) await deleteRL.consume(`delete:${address}`);
    const body = await c.req.json();

    // Look up the stored identity and verify the signature
    const identity = await store.getIdentity(address);
    if (!identity) {
      return c.json({ error: 'Address not registered', code: 'SHADE_NOT_FOUND' }, 404);
    }
    // Include address in the signed payload
    await verifyPayload(crypto, identity.identitySigningKey, { ...body, address });

    await store.deleteAll(address);
    if (kt) {
      await kt.recordDelete(address);
    }
    events?.emit('server.identity_deleted', { address });
    return c.json({ ok: true });
  });

  if (kt) {
    app.route('/v1/kt', createKTRoutes(kt));
  }

  return app;
}

// ─── Base64 helpers ──────────────────────────────────────────

function bytesToB64(bytes: Uint8Array): string {
  return Buffer.from(bytes).toString('base64');
}

function b64ToBytes(b64: string): Uint8Array {
  return new Uint8Array(Buffer.from(b64, 'base64'));
}
