import type { MiddlewareHandler } from 'hono';
import { createRouter } from '../../create-router';
import { originMiddleware, userMiddleware } from '../../_middlewares';
import { isSuperAdmin } from '../../super-admin';
import * as errors from '../../custom-errors';
import { getVaultServerUrl } from './sidecar-server';
import { stripHopByHop, redactPath } from './proxy-util';
import { passwordGrant, pick, expiryFrom, type PasswordGrantInput } from './broker';
import { getValidAccessToken, refreshAccess } from './token-store';
import { getVaultTokens, setVaultTokens, getVaultUnlockKey, setVaultUnlockKey } from 'officerdb';

// Platform side of Officer Vault (VAULT_AUTH_SPEC.md). The device holds NO Vaultwarden token; a valid
// platform session authorizes vault access. This router:
//   • gates every /api/vault/* on an owner platform session (userMiddleware, no bodyParser → streaming),
//   • serves the native broker/unlock-key endpoints,
//   • proxies the rest to the officer-vault sidecar, swapping the incoming platform JWT for the stored
//     Vaultwarden token. Bodies are never parsed/decrypted; only the Authorization header is rewritten.

export const vaultRouter = createRouter();

const PREFIX = '/api/vault';

// Owner-only. The global backstop already blocks non-owner tokens from /api/vault; this is explicit
// defense-in-depth and gives a clear error.
const ownerGate: MiddlewareHandler = async (ctx, next) => {
  if (!(await isSuperAdmin(ctx.get('user')))) throw errors.FORBIDDEN('Vault is owner-only');
  return next();
};

vaultRouter.use(originMiddleware); // set ctx 'origin' for userMiddleware's origin check
vaultRouter.use(userMiddleware); // valid platform session (Authorization: Bearer <platform JWT> or ?token=)
vaultRouter.use(ownerGate);

// ── Native endpoints (registered before the catch-all) ──

// Reachability probe (owner-gated): forwards to the sidecar's /_health, which probes Vaultwarden's /alive.
vaultRouter.get('/_health', async (ctx) => {
  const base = getVaultServerUrl();
  if (!base) return ctx.json({ ok: false, error: 'vault sidecar unavailable' }, 503);
  try {
    const r = await fetch(`${base}/_health`, { signal: AbortSignal.timeout(5000) });
    const body = await r.json().catch(() => ({ ok: r.ok }));
    return Response.json(body, { status: r.status });
  } catch {
    return ctx.json({ ok: false, error: 'vault sidecar unreachable' }, 502);
  }
});

// Token broker — exchange the client-derived auth hash for a Vaultwarden token set (stored server-side),
// and return the encrypted user-key material the client needs to unlock on-device.
vaultRouter.post('/session/login', async (ctx) => {
  const user = ctx.get('user');
  const b = (await ctx.req.json().catch(() => ({}))) as Record<string, unknown>;
  const str = (v: unknown) => (typeof v === 'string' ? v : '');
  const email = str(b.email);
  const authHash = str(b.authHash);
  if (!email || !authHash) return ctx.json({ ok: false, error: 'email and authHash required' }, 400);

  const clientId = str(b.clientId) || 'mobile';
  const input: PasswordGrantInput = {
    email,
    authHash,
    clientId,
    deviceIdentifier: str(b.deviceIdentifier),
    deviceType: b.deviceType != null ? String(b.deviceType) : '',
    deviceName: str(b.deviceName),
  };
  const result = await passwordGrant(input);
  if (!result.ok) return Response.json({ ok: false, error: result.error }, { status: result.status });

  const d = result.data;
  const access = pick(d, 'access_token');
  const refresh = pick(d, 'refresh_token');
  if (typeof access !== 'string' || typeof refresh !== 'string') {
    return ctx.json({ ok: false, error: 'vault returned no token' }, 502);
  }
  await setVaultTokens(user.id, {
    accessToken: access,
    refreshToken: refresh,
    expiresAt: expiryFrom(d, Date.now()),
    deviceIdentifier: input.deviceIdentifier || null,
    clientId,
  });

  // Everything except the transport tokens (which the platform holds) is crypto material the SDK unlocks
  // on-device — ciphertext to us. Vaultwarden returns BOTH classic (Key/PrivateKey/Kdf*) and v2
  // (UserDecryptionOptions.MasterPasswordUnlock, AccountKeys); pass the whole native response through
  // under `connectToken` so the SDK gets whatever it uses, alongside the spec's named aliases.
  const connectToken: Record<string, unknown> = { ...d };
  for (const k of ['access_token', 'refresh_token', 'token_type', 'expires_in']) delete connectToken[k];
  return ctx.json({
    ok: true,
    protectedUserKey: pick(d, 'Key') ?? null,
    privateKey: pick(d, 'PrivateKey') ?? null,
    kdf: {
      kdf: pick(d, 'Kdf') ?? null,
      kdfIterations: pick(d, 'KdfIterations') ?? null,
      kdfMemory: pick(d, 'KdfMemory') ?? null,
      kdfParallelism: pick(d, 'KdfParallelism') ?? null,
    },
    connectToken,
  });
});

// Officer-app unlock path: store / release the platform-held protector key (owner session only).
vaultRouter.put('/unlock-key', async (ctx) => {
  const user = ctx.get('user');
  const b = (await ctx.req.json().catch(() => ({}))) as Record<string, unknown>;
  if (typeof b.wrappedKey !== 'string' || !b.wrappedKey) return ctx.json({ error: 'wrappedKey required' }, 400);
  await setVaultUnlockKey(user.id, b.wrappedKey);
  return ctx.json({ ok: true });
});

vaultRouter.get('/unlock-key', async (ctx) => {
  const user = ctx.get('user');
  const wrappedKey = await getVaultUnlockKey(user.id);
  if (!wrappedKey) return ctx.json({ error: 'no unlock key stored' }, 404);
  return ctx.json({ wrappedKey });
});

// ── Token-injecting proxy → sidecar → Vaultwarden ──
vaultRouter.all('/*', async (ctx) => {
  const base = getVaultServerUrl();
  if (!base) return ctx.text('Vault sidecar not available', 503);

  const user = ctx.get('user');
  const url = new URL(ctx.req.url);
  const subpath = url.pathname.slice(PREFIX.length) || '/';
  const target = `${base}${subpath}${url.search}`;
  const method = ctx.req.method;
  const hasBody = method !== 'GET' && method !== 'HEAD';
  const started = Date.now();

  // Inject a valid Vaultwarden token (proactively refreshed near expiry). No token yet → forward without
  // one: unauthenticated endpoints (prelogin) work; authed ones get Vaultwarden's 401.
  const accessToken = await getValidAccessToken(user.id);

  const headers = stripHopByHop(ctx.req.raw.headers);
  headers.delete('authorization'); // drop the platform JWT
  if (accessToken) headers.set('authorization', `Bearer ${accessToken}`);

  const init: RequestInit & { duplex?: 'half' } = { method, headers, redirect: 'manual' };
  if (hasBody) {
    init.body = ctx.req.raw.body;
    init.duplex = 'half';
  }

  let upstream: Response;
  try {
    upstream = await fetch(target, init);
  } catch {
    console.error(`[vault] ${method} ${redactPath(subpath + url.search)} -> sidecar unreachable`);
    return ctx.text('Vault sidecar unreachable', 502);
  }

  // A 401 on a replayable (no-body) request → token likely just expired/revoked: refresh once and retry.
  if (upstream.status === 401 && !hasBody) {
    const ts = await getVaultTokens(user.id);
    const fresh = ts ? await refreshAccess(user.id, ts) : null;
    if (fresh) {
      headers.set('authorization', `Bearer ${fresh}`);
      upstream = await fetch(target, { method, headers, redirect: 'manual' }).catch(() => upstream);
    }
  }

  console.log(`[vault] ${method} ${redactPath(subpath)} -> ${upstream.status} ${Date.now() - started}ms`);
  return new Response(upstream.body, { status: upstream.status, headers: stripHopByHop(upstream.headers) });
});
