import { type Application, type Request, type Response, type NextFunction } from 'express';
import express from 'express';
import { logger } from '../logger.js';
import type { Repository, NotifyEventType, NotificationPrefsUpdate } from '../db/repository.js';
import type { VapidKeyStore } from '../vapid-store.js';
import type { PushService } from '../push-service.js';

/**
 * `/api/notifications/*` routes for Web Push V2.
 * Spec: docs/superpowers/specs/2026-05-28-browser-notifications-v2-webpush.md.
 *
 * When `pushService` is null (push.enabled === false), all POST/DELETE/test
 * endpoints return 503; read endpoints still work so the UI can show
 * "管理者により無効化されています".
 */

type AuthedUser = { id: string; role?: string };

function getUser(req: Request): AuthedUser | undefined {
  return (req as unknown as { user?: AuthedUser }).user;
}

function requireUser(req: Request, res: Response): AuthedUser | null {
  const user = getUser(req);
  if (!user) {
    res.status(401).json({ error: 'auth required' });
    return null;
  }
  return user;
}

// ── Per-user in-memory sliding-window rate limiter ─────────────────────
// Maps `${key}:${userId}` → array of recent request timestamps (ms).
// Window is fixed at 1 hour; sufficient for low-frequency notification ops.
const rateBuckets = new Map<string, number[]>();
const ONE_HOUR_MS = 60 * 60 * 1000;

function rateLimit(key: string, maxPerHour: number) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const user = getUser(req);
    if (!user) {
      res.status(401).json({ error: 'auth required' });
      return;
    }
    const bucketKey = `${key}:${user.id}`;
    const now = Date.now();
    const cutoff = now - ONE_HOUR_MS;
    const bucket = (rateBuckets.get(bucketKey) ?? []).filter(t => t > cutoff);
    if (bucket.length >= maxPerHour) {
      res.status(429).json({
        error: 'rate limit exceeded',
        retryAfter: Math.ceil((bucket[0]! + ONE_HOUR_MS - now) / 1000),
      });
      return;
    }
    bucket.push(now);
    rateBuckets.set(bucketKey, bucket);
    next();
  };
}

/** Test hook — only used by unit tests. */
export function resetRateLimitsForTest(): void {
  rateBuckets.clear();
}

// ── Input validation ──────────────────────────────────────────────────

function validateSubscriptionInput(body: unknown): {
  endpoint: string;
  p256dh: string;
  auth: string;
  userAgent?: string;
} | { error: string } {
  if (!body || typeof body !== 'object') return { error: 'body required' };
  const b = body as Record<string, unknown>;
  if (typeof b.endpoint !== 'string') return { error: 'endpoint required' };
  if (!b.endpoint.startsWith('https://')) return { error: 'endpoint must be https' };
  if (b.endpoint.length > 2048) return { error: 'endpoint too long' };
  if (typeof b.p256dh !== 'string' || b.p256dh.length === 0 || b.p256dh.length > 200) {
    return { error: 'p256dh must be 1..200 chars' };
  }
  if (typeof b.auth !== 'string' || b.auth.length === 0 || b.auth.length > 200) {
    return { error: 'auth must be 1..200 chars' };
  }
  const ua = typeof b.userAgent === 'string' ? b.userAgent.slice(0, 200) : undefined;
  return { endpoint: b.endpoint, p256dh: b.p256dh, auth: b.auth, ...(ua ? { userAgent: ua } : {}) };
}

function validatePrefsInput(body: unknown): NotificationPrefsUpdate | { error: string } {
  if (!body || typeof body !== 'object') return { error: 'body required' };
  const b = body as Record<string, unknown>;
  const update: NotificationPrefsUpdate = {};
  if (b.enabled !== undefined) {
    if (typeof b.enabled !== 'boolean') return { error: 'enabled must be boolean' };
    update.enabled = b.enabled;
  }
  if (b.events !== undefined) {
    if (!b.events || typeof b.events !== 'object') return { error: 'events must be object' };
    const events = b.events as Record<string, unknown>;
    const eventUpdate: Partial<Record<NotifyEventType, boolean>> = {};
    for (const key of ['running', 'succeeded', 'failed', 'waiting_human'] as const) {
      if (events[key] !== undefined) {
        if (typeof events[key] !== 'boolean') return { error: `events.${key} must be boolean` };
        eventUpdate[key] = events[key] as boolean;
      }
    }
    update.events = eventUpdate as Record<NotifyEventType, boolean>;
  }
  if (b.includeDetails !== undefined) {
    if (typeof b.includeDetails !== 'boolean') return { error: 'includeDetails must be boolean' };
    update.includeDetails = b.includeDetails;
  }
  return update;
}

// ── Public DTOs (do NOT leak p256dh / auth / privateKey / etc.) ────────

function toPublicSubscription(sub: {
  id: string;
  endpoint: string;
  userAgent: string | null;
  createdAt: string;
  lastSuccessAt: string | null;
  lastFailureAt: string | null;
  failureCount: number;
}) {
  return {
    id: sub.id,
    // Truncate endpoint to scheme + host for UI display; full URL is sensitive.
    endpointHost: (() => {
      try { return new URL(sub.endpoint).host; } catch { return 'unknown'; }
    })(),
    userAgent: sub.userAgent,
    createdAt: sub.createdAt,
    lastSuccessAt: sub.lastSuccessAt,
    lastFailureAt: sub.lastFailureAt,
    failureCount: sub.failureCount,
  };
}

// ── Mount ──────────────────────────────────────────────────────────────

export interface NotificationsApiDeps {
  repo: Repository;
  pushService: PushService | null;
  vapidStore: VapidKeyStore | null;
  /** Plugged in by server.ts when auth is active; identity transform otherwise. */
  requireAuth: (req: Request, res: Response, next: NextFunction) => void;
}

export function mountNotificationsApi(app: Application, deps: NotificationsApiDeps): void {
  const { repo, pushService, vapidStore, requireAuth } = deps;
  const json = express.json({ limit: '64kb' });

  // GET /vapid-public-key — always 200 when push is enabled; required before subscribe.
  app.get('/api/notifications/vapid-public-key', requireAuth, (req, res) => {
    const user = requireUser(req, res); if (!user) return;
    if (!pushService || !vapidStore) {
      res.status(503).json({ error: 'push disabled' });
      return;
    }
    const k = vapidStore.getCurrent();
    res.json({ publicKey: k.publicKey, keyId: k.keyId });
  });

  // GET /subscriptions — caller's own devices.
  app.get('/api/notifications/subscriptions', requireAuth, (req, res) => {
    const user = requireUser(req, res); if (!user) return;
    const subs = repo.listPushSubscriptionsForUser(user.id);
    res.json({ subscriptions: subs.map(toPublicSubscription) });
  });

  // POST /subscriptions — register/upsert (endpoint UNIQUE moves ownership).
  app.post(
    '/api/notifications/subscriptions',
    requireAuth,
    json,
    rateLimit('push:subscribe', 10),
    (req, res) => {
      const user = requireUser(req, res); if (!user) return;
      if (!pushService || !vapidStore) {
        res.status(503).json({ error: 'push disabled' });
        return;
      }
      const parsed = validateSubscriptionInput(req.body);
      if ('error' in parsed) {
        res.status(400).json({ error: parsed.error });
        return;
      }
      const current = vapidStore.getCurrent();
      const { id } = repo.upsertPushSubscription({
        userId: user.id,
        endpoint: parsed.endpoint,
        p256dh: parsed.p256dh,
        auth: parsed.auth,
        userAgent: parsed.userAgent ?? null,
        vapidKeyId: current.keyId,
      });
      res.json({ id });
    },
  );

  // DELETE /subscriptions/:id — only delete your own.
  app.delete(
    '/api/notifications/subscriptions/:id',
    requireAuth,
    rateLimit('push:unsubscribe', 30),
    (req, res) => {
      const user = requireUser(req, res); if (!user) return;
      const sub = repo.getPushSubscriptionById(req.params.id!);
      if (!sub || sub.userId !== user.id) {
        res.status(404).json({ error: 'not found' });
        return;
      }
      repo.deletePushSubscription(sub.id);
      res.json({ ok: true });
    },
  );

  // GET /preferences — auto-creates a default row on first access.
  app.get('/api/notifications/preferences', requireAuth, (req, res) => {
    const user = requireUser(req, res); if (!user) return;
    res.json(repo.getUserNotificationPrefs(user.id));
  });

  // PUT /preferences — partial update.
  app.put(
    '/api/notifications/preferences',
    requireAuth,
    json,
    rateLimit('push:prefs', 30),
    (req, res) => {
      const user = requireUser(req, res); if (!user) return;
      const parsed = validatePrefsInput(req.body);
      if ('error' in parsed) {
        res.status(400).json({ error: parsed.error });
        return;
      }
      repo.upsertUserNotificationPrefs(user.id, parsed);
      res.json(repo.getUserNotificationPrefs(user.id));
    },
  );

  // POST /preferences/migrate-from-localstorage — one-shot V1 → V2.
  app.post(
    '/api/notifications/preferences/migrate-from-localstorage',
    requireAuth,
    json,
    rateLimit('push:migrate', 3),
    (req, res) => {
      const user = requireUser(req, res); if (!user) return;
      const parsed = validatePrefsInput(req.body);
      if ('error' in parsed) {
        res.status(400).json({ error: parsed.error });
        return;
      }
      const flipped = repo.markV1MigrationComplete(user.id);
      if (!flipped) {
        res.status(409).json({ error: 'already migrated' });
        return;
      }
      repo.upsertUserNotificationPrefs(user.id, parsed);
      res.json({ ok: true, prefs: repo.getUserNotificationPrefs(user.id) });
    },
  );

  // POST /test — send a test push.
  app.post(
    '/api/notifications/test',
    requireAuth,
    rateLimit('push:test', 5),
    (req, res) => {
      const user = requireUser(req, res); if (!user) return;
      if (!pushService) {
        res.status(503).json({ error: 'push disabled' });
        return;
      }
      pushService.enqueue({
        event: 'succeeded',
        taskId: 0,
        taskTitle: 'テスト通知',
        pieceName: 'V2 Web Push 動作確認',
        ownerId: user.id,
      });
      res.json({ ok: true });
    },
  );

  logger.info('[notifications-api] mounted (/api/notifications/*)');
}
