import { Hono } from 'hono';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import {
  createInboxRoutes,
  createBridgeRoutes,
  createBlobRoutes,
  InboxServerEvents,
  InboxPruneTask,
  MemoryInboxStore,
  MemoryBlobStore,
  type InboxStore,
  type BlobStore,
} from '@shade/inbox-server';
import { createPrekeyRoutes } from './routes.js';
import { createHealthRoutes } from './health.js';
import { createMetricsRoutes, metricsMiddleware } from './metrics.js';
import { createOpenApiRoutes } from './openapi.js';
import { PrekeyServerEvents } from './events.js';
import { StaleCleanupTask } from './cleanup.js';
import { logger } from './logger.js';
import type { PrekeyStore } from './store.js';
import { KeyTransparencyService } from './kt-integration.js';
import {
  MemoryKTLogStore,
  type KTLogStore,
} from '@shade/key-transparency';

const VERSION = '4.0.0';

async function createStore(): Promise<PrekeyStore & { close?: () => void | Promise<void> }> {
  const sqlitePath = process.env.SHADE_PREKEY_DB_PATH;
  const pgUrl = process.env.SHADE_PREKEY_PG_URL;

  if (pgUrl) {
    const { PostgresPrekeyStore } = await import('@shade/storage-postgres');
    logger.info('Using PostgreSQL prekey store', { url: maskUrl(pgUrl) });
    return PostgresPrekeyStore.create(pgUrl);
  }

  if (sqlitePath) {
    const { SqlitePrekeyStore } = await import('@shade/storage-sqlite');
    logger.info('Using SQLite prekey store', { path: sqlitePath });
    return new SqlitePrekeyStore(sqlitePath);
  }

  const { MemoryPrekeyStore } = await import('./memory-store.js');
  logger.warn('Using in-memory prekey store — data will not persist across restarts');
  return new MemoryPrekeyStore();
}

async function createInboxStore(): Promise<InboxStore & { close?: () => void | Promise<void> }> {
  const sqlitePath = process.env.SHADE_INBOX_DB_PATH;
  const pgUrl = process.env.SHADE_INBOX_PG_URL ?? process.env.SHADE_PREKEY_PG_URL;

  if (pgUrl && process.env.SHADE_INBOX_PG_URL) {
    const { PostgresInboxStore } = await import('@shade/storage-postgres');
    logger.info('Using PostgreSQL inbox store', { url: maskUrl(pgUrl) });
    return PostgresInboxStore.create(pgUrl);
  }

  if (sqlitePath) {
    const { SqliteInboxStore } = await import('@shade/storage-sqlite');
    logger.info('Using SQLite inbox store', { path: sqlitePath });
    return new SqliteInboxStore(sqlitePath);
  }

  if (pgUrl) {
    const { PostgresInboxStore } = await import('@shade/storage-postgres');
    logger.info('Using PostgreSQL inbox store (sharing prekey URL)', { url: maskUrl(pgUrl) });
    return PostgresInboxStore.create(pgUrl);
  }

  logger.warn('Using in-memory inbox store — data will not persist across restarts');
  return new MemoryInboxStore();
}

/**
 * V4.9 — encrypted-blob primitive (`/v1/blob/<slotId>`).
 *
 * Backend selection mirrors the inbox store: an explicit
 * `SHADE_BLOB_PG_URL` wins, then a SQLite path, then we fall back to the
 * shared `SHADE_PREKEY_PG_URL` if present, then memory. Operators can
 * also opt the blob store *off* entirely via `SHADE_DISABLE_BLOB=1` —
 * useful for relays that only want the inbox surface.
 */
async function createBlobStore(): Promise<BlobStore & { close?: () => void | Promise<void> }> {
  const sqlitePath = process.env.SHADE_BLOB_DB_PATH;
  const pgUrl = process.env.SHADE_BLOB_PG_URL ?? process.env.SHADE_PREKEY_PG_URL;

  if (pgUrl && process.env.SHADE_BLOB_PG_URL) {
    const { PostgresBlobStore } = await import('@shade/storage-postgres');
    logger.info('Using PostgreSQL blob store', { url: maskUrl(pgUrl) });
    return PostgresBlobStore.create(pgUrl);
  }

  if (sqlitePath) {
    const { SqliteBlobStore } = await import('@shade/storage-sqlite');
    logger.info('Using SQLite blob store', { path: sqlitePath });
    return new SqliteBlobStore(sqlitePath);
  }

  if (pgUrl) {
    const { PostgresBlobStore } = await import('@shade/storage-postgres');
    logger.info('Using PostgreSQL blob store (sharing prekey URL)', { url: maskUrl(pgUrl) });
    return PostgresBlobStore.create(pgUrl);
  }

  logger.warn('Using in-memory blob store — data will not persist across restarts');
  return new MemoryBlobStore();
}

async function maybeCreateKT(): Promise<KeyTransparencyService | undefined> {
  const skPriv = process.env.SHADE_KT_SIGNING_PRIVATE_KEY;
  const skPub = process.env.SHADE_KT_SIGNING_PUBLIC_KEY;
  if (!skPriv || !skPub) {
    if (skPriv || skPub) {
      logger.warn(
        'Key Transparency requires BOTH SHADE_KT_SIGNING_PRIVATE_KEY and SHADE_KT_SIGNING_PUBLIC_KEY — KT disabled',
      );
    } else {
      logger.info('Key Transparency disabled (signing keys not configured)');
    }
    return undefined;
  }

  let signingPrivateKey: Uint8Array;
  let signingPublicKey: Uint8Array;
  try {
    signingPrivateKey = new Uint8Array(Buffer.from(skPriv, 'base64'));
    signingPublicKey = new Uint8Array(Buffer.from(skPub, 'base64'));
  } catch {
    logger.error('SHADE_KT_SIGNING_*_KEY must be base64 — KT disabled');
    return undefined;
  }
  if (signingPrivateKey.length !== 32 || signingPublicKey.length !== 32) {
    logger.error(
      `SHADE_KT_SIGNING_*_KEY must decode to 32 bytes (priv=${signingPrivateKey.length}, pub=${signingPublicKey.length}) — KT disabled`,
    );
    return undefined;
  }

  let ktStore: KTLogStore;
  const ktPg = process.env.SHADE_KT_PG_URL ?? process.env.SHADE_PREKEY_PG_URL;
  if (ktPg) {
    const { PostgresKTLogStore } = await import('@shade/storage-postgres');
    logger.info('Using PostgreSQL KT log store', { url: maskUrl(ktPg) });
    ktStore = await PostgresKTLogStore.create(ktPg);
  } else {
    logger.warn('Using in-memory KT log store — KT data will not persist across restarts');
    ktStore = new MemoryKTLogStore();
  }

  const heartbeatRaw = process.env.SHADE_KT_HEARTBEAT_MS;
  const heartbeatIntervalMs = heartbeatRaw ? Number(heartbeatRaw) : 10 * 60 * 1000;

  const svc = await KeyTransparencyService.create(crypto, {
    store: ktStore,
    signingPrivateKey,
    signingPublicKey,
    heartbeatIntervalMs,
  });
  logger.info('Key Transparency enabled', {
    logId: Buffer.from(svc.getLogId()).toString('hex').slice(0, 16) + '…',
    heartbeatIntervalMs,
  });
  return svc;
}

function maskUrl(url: string): string {
  try {
    const u = new URL(url);
    if (u.password) u.password = '***';
    return u.toString();
  } catch {
    return '***';
  }
}

const crypto = new SubtleCryptoProvider();
const store = await createStore();
const events = new PrekeyServerEvents();

// Inbox store + events (V3.6 store-and-forward relay)
const inboxStore = await createInboxStore();
const inboxEvents = new InboxServerEvents();

// ─── Optional: Key Transparency (V3.12) ──────────────────────
//
// Enabled when both SHADE_KT_SIGNING_PRIVATE_KEY and
// SHADE_KT_SIGNING_PUBLIC_KEY are set (base64-encoded 32-byte
// Ed25519 seeds). Storage: PostgresKTLogStore when
// SHADE_KT_PG_URL (or SHADE_PREKEY_PG_URL) is set, else memory.
const kt = await maybeCreateKT();

// V4.8.1 — `SHADE_DISABLE_RATE_LIMIT=1` turns off the IP-based
// register/replenish/fetch token-buckets on every prekey + inbox
// route. INTENDED FOR SELF-HOSTED SINGLE-TEAM (DEV / SINGLE-TENANT)
// DEPLOYMENTS ONLY — the rate-limit defends multi-tenant relays
// against abuse, so a public/shared deployment must leave this
// unset. Without it, the existing 5/hour REGISTER_LIMIT etc. apply
// unchanged.
const disableRateLimit = process.env.SHADE_DISABLE_RATE_LIMIT === '1';
if (disableRateLimit) {
  logger.warn(
    'SHADE_DISABLE_RATE_LIMIT=1 — IP rate limits OFF on prekey + inbox routes. Use only for single-tenant deployments.',
  );
}

// Compose the full app: metrics middleware + health + metrics + prekey routes
const app = new Hono();
app.use('*', metricsMiddleware());
app.route('/', createHealthRoutes(store, VERSION));
app.route('/', createMetricsRoutes());
app.route('/', createOpenApiRoutes());
app.route(
  '/',
  createPrekeyRoutes(store, crypto, {
    events,
    disableRateLimit,
    ...(kt ? { keyTransparency: kt } : {}),
  }),
);
// V3.7 transport-bridge — SSE / long-poll / WS fallbacks for the inbox.
// Created BEFORE the inbox routes so the shared bridge-delivery log can
// be wired into both. The log is the cross-channel dedup gate that lets
// the inbox-fetch route skip blobs already pushed via bridge — see
// V4.8.4 changelog and the Prism FR
// `cross-channel-duplicate-fanout-v4.8.2.md`.
const bridgeRoutes = createBridgeRoutes({
  store: inboxStore,
  crypto,
  events: inboxEvents,
});

app.route(
  '/',
  createInboxRoutes(inboxStore, crypto, {
    events: inboxEvents,
    disableRateLimit,
    bridgeDeliveryLog: bridgeRoutes.bridgeDeliveryLog,
  }),
);
app.route('/', bridgeRoutes.app);

// V4.9 — encrypted-blob primitive. Powers Prism's credential-driven
// device-linking (Phase 2) and any other Shade app that needs a
// "sign in from any device" UX. Mounted on the same Hono app so a
// single relay process serves prekey + inbox + blob from one port.
const blobDisabled = process.env.SHADE_DISABLE_BLOB === '1';
const blobStore = blobDisabled ? null : await createBlobStore();
if (blobDisabled) {
  logger.info('Blob primitive disabled (SHADE_DISABLE_BLOB=1)');
} else if (blobStore) {
  app.route('/', createBlobRoutes(blobStore, crypto, { disableRateLimit }));
  logger.info('Blob primitive enabled', { route: '/v1/blob/:slotId' });
}

// ─── Optional: Observer + Dashboard ──────────────────────────

const observerToken = process.env.SHADE_OBSERVER_TOKEN;
if (observerToken && observerToken.length >= 16) {
  try {
    const { createObserver } = await import('@shade/observer');
    const observer = createObserver({
      token: observerToken,
      serverEvents: events,
    });
    app.route('/shade-observer', observer);
    logger.info('Observer enabled', { path: '/shade-observer/dashboard/' });
  } catch (err) {
    logger.warn('Observer module not available, skipping', { error: String(err) });
  }
} else if (observerToken) {
  logger.warn('SHADE_OBSERVER_TOKEN is set but too short (needs ≥16 chars), observer disabled');
} else {
  logger.info('Observer disabled (SHADE_OBSERVER_TOKEN not set)');
}

// ─── Stale cleanup task ──────────────────────────────────────

const cleanupTask = new StaleCleanupTask(store);
cleanupTask.start();
logger.info('Stale cleanup task started', {
  staleDays: Number(process.env.SHADE_STALE_DAYS ?? 30),
  intervalHours: Number(process.env.SHADE_CLEANUP_INTERVAL_HOURS ?? 24),
});

// ─── Inbox prune task ────────────────────────────────────────

const inboxPrune = new InboxPruneTask(inboxStore, {
  events: inboxEvents,
  logger: {
    info: (m, d) => logger.info(m, d as Record<string, unknown> | undefined),
    error: (m, d) => logger.error(m, d as Record<string, unknown> | undefined),
  },
});
inboxPrune.start();
logger.info('Inbox prune task started', {
  intervalMinutes: Number(process.env.SHADE_INBOX_PRUNE_INTERVAL_MINUTES ?? 5),
});

// ─── Start HTTP server ───────────────────────────────────────

const port = Number(process.env.PORT ?? 3900);

logger.info('Shade Prekey Server starting', { port, version: VERSION });

const server = Bun.serve({
  port,
  fetch: (req, srv) => app.fetch(req, srv),
  websocket: bridgeRoutes.websocket as any,
});

// ─── Graceful shutdown ───────────────────────────────────────

let shuttingDown = false;
async function shutdown(signal: string) {
  if (shuttingDown) return;
  shuttingDown = true;
  logger.info('Shutting down', { signal });

  try {
    cleanupTask.stop();
    inboxPrune.stop();
    server.stop();
    if ('close' in store && typeof store.close === 'function') {
      await store.close();
    }
    if ('close' in inboxStore && typeof inboxStore.close === 'function') {
      await inboxStore.close();
    }
    if (blobStore && 'close' in blobStore && typeof blobStore.close === 'function') {
      await blobStore.close();
    }
    logger.info('Shutdown complete');
    process.exit(0);
  } catch (err) {
    logger.error('Error during shutdown', { error: String(err) });
    process.exit(1);
  }
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

logger.info('Shade Prekey Server listening', { port });
