import express from "express";
import { randomUUID, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import { Pool } from "pg";

const config = readConfig();
const pool = new Pool({ connectionString: config.databaseUrl, max: config.databasePoolSize });
const app = express();
const httpServer = createServer(app);
const streamClients = new Set();

app.disable("x-powered-by");
app.use(express.json({ limit: "1mb" }));

app.get("/healthz", asyncRoute(async (_req, res) => {
  await pool.query("select 1");
  res.json({
    ok: true,
    service: "nodedc-notification-core",
    database: "ready",
    internalApiConfigured: Boolean(config.internalAccessToken),
  });
}));

app.post("/internal/notifications/events", requireInternalApi, asyncRoute(async (req, res) => {
  const command = sanitizeEventCommand(req.body);
  const result = await createNotificationEvent(command);
  res.status(result.created ? 201 : 200).json(result);
}));

app.get("/api/notifications", requireInternalApi, asyncRoute(async (req, res) => {
  const subject = getRequestSubject(req);
  const surface = sanitizeSurface(req.query.surface || "hub");
  const limit = sanitizeLimit(req.query.limit);
  const unreadOnly = req.query.unreadOnly === "1" || req.query.unreadOnly === "true";
  const deliveries = await listDeliveries({ subject, surface, limit, unreadOnly });
  res.json({ ok: true, surface, deliveries });
}));

app.post("/api/notifications/:deliveryId/read", requireInternalApi, asyncRoute(async (req, res) => {
  const subject = getRequestSubject(req);
  const deliveryId = sanitizeUuid(req.params.deliveryId, "deliveryId");
  const delivery = await markDeliveryRead({ subject, deliveryId });
  if (!delivery) {
    res.status(404).json({ ok: false, error: "notification_delivery_not_found" });
    return;
  }
  broadcastDeliveryUpdate("notification.delivery.read", delivery);
  res.json({ ok: true, delivery });
}));

app.post("/api/notifications/read-all", requireInternalApi, asyncRoute(async (req, res) => {
  const subject = getRequestSubject(req);
  const surface = sanitizeSurface(req.query.surface || req.body?.surface || "hub");
  const result = await markAllDeliveriesRead({ subject, surface });
  broadcastSubjectUpdate("notification.deliveries.read-all", { subject, surface, count: result.count });
  res.json({ ok: true, surface, count: result.count });
}));

app.get("/api/notifications/stream", requireInternalApi, (req, res) => {
  const subject = getRequestSubject(req);
  const surface = sanitizeSurface(req.query.surface || "hub");
  const client = { id: randomUUID(), res, subject, surface };

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache, no-transform");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("X-Accel-Buffering", "no");
  res.flushHeaders?.();
  writeSse(res, "notification.ready", { ok: true, surface });

  const keepAlive = setInterval(() => {
    res.write(": keep-alive\n\n");
  }, 30000);

  streamClients.add(client);
  req.on("close", () => {
    clearInterval(keepAlive);
    streamClients.delete(client);
  });
});

app.use((error, _req, res, _next) => {
  const status = Number(error?.status || 500);
  const message = error instanceof Error ? error.message : "internal_error";
  res.status(status >= 400 && status < 600 ? status : 500).json({ ok: false, error: message });
});

await migrate();

httpServer.listen(config.port, "0.0.0.0", () => {
  console.log(`NODE.DC Notification Core listening on http://0.0.0.0:${config.port}`);
});

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

async function createNotificationEvent(command) {
  const client = await pool.connect();
  try {
    await client.query("begin");

    let event = null;
    if (command.idempotencyKey) {
      const existing = await client.query(
        "select * from notification_events where idempotency_key = $1",
        [command.idempotencyKey]
      );
      if (existing.rowCount > 0) {
        event = existing.rows[0];
        const deliveries = await loadDeliveriesForEvent(client, event.id);
        await client.query("commit");
        return { ok: true, created: false, idempotent: true, event: toEvent(event), deliveries };
      }
    }

    const eventId = randomUUID();
    const insertedEvent = await client.query(
      `insert into notification_events (
        id,
        type,
        source_service,
        actor_user_id,
        actor_email,
        subject_type,
        subject_id,
        payload,
        idempotency_key
      ) values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
      returning *`,
      [
        eventId,
        command.type,
        command.sourceService,
        command.actor.userId,
        command.actor.email,
        command.subject.type,
        command.subject.id,
        JSON.stringify(command.payload),
        command.idempotencyKey,
      ]
    );
    event = insertedEvent.rows[0];

    const deliveries = [];
    for (const delivery of command.deliveries) {
      const insertedDelivery = await client.query(
        `insert into notification_deliveries (
          id,
          event_id,
          recipient_user_id,
          recipient_email,
          surface,
          title,
          body,
          meta,
          status,
          actionable,
          action_type,
          action_url,
          entity_type,
          entity_id
        ) values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12, $13, $14)
        returning *`,
        [
          randomUUID(),
          event.id,
          delivery.recipientUserId,
          delivery.recipientEmail,
          delivery.surface,
          delivery.title,
          delivery.body,
          JSON.stringify(delivery.meta),
          delivery.status,
          delivery.actionable,
          delivery.actionType,
          delivery.actionUrl,
          delivery.entityType,
          delivery.entityId,
        ]
      );
      deliveries.push(toDelivery({ ...insertedDelivery.rows[0], event_type: event.type, source_service: event.source_service, event_payload: event.payload }));
    }

    await client.query("commit");
    for (const delivery of deliveries) {
      broadcastDeliveryUpdate("notification.delivery.created", delivery);
    }
    return { ok: true, created: true, event: toEvent(event), deliveries };
  } catch (error) {
    await client.query("rollback");
    throw error;
  } finally {
    client.release();
  }
}

async function listDeliveries({ subject, surface, limit, unreadOnly }) {
  const values = [surface, subject.userId, subject.email, limit];
  const unreadSql = unreadOnly ? "and d.status = 'unread'" : "";
  const result = await pool.query(
    `select
      d.*,
      e.type as event_type,
      e.source_service,
      e.payload as event_payload
    from notification_deliveries d
    join notification_events e on e.id = d.event_id
    where d.surface = $1
      and (
        ($2::text is not null and d.recipient_user_id = $2)
        or ($3::text is not null and lower(d.recipient_email) = lower($3))
      )
      ${unreadSql}
    order by d.created_at desc
    limit $4`,
    values
  );
  return result.rows.map(toDelivery);
}

async function markDeliveryRead({ subject, deliveryId }) {
  const result = await pool.query(
    `update notification_deliveries
    set status = 'read', read_at = coalesce(read_at, now()), updated_at = now()
    where id = $1
      and status = 'unread'
      and (
        ($2::text is not null and recipient_user_id = $2)
        or ($3::text is not null and lower(recipient_email) = lower($3))
      )
    returning *`,
    [deliveryId, subject.userId, subject.email]
  );
  if (result.rowCount > 0) {
    const delivery = await loadDelivery(result.rows[0].id);
    return delivery;
  }

  const existing = await loadDeliveryForSubject({ subject, deliveryId });
  return existing?.status === "read" ? existing : null;
}

async function markAllDeliveriesRead({ subject, surface }) {
  const result = await pool.query(
    `update notification_deliveries
    set status = 'read', read_at = coalesce(read_at, now()), updated_at = now()
    where surface = $1
      and status = 'unread'
      and (
        ($2::text is not null and recipient_user_id = $2)
        or ($3::text is not null and lower(recipient_email) = lower($3))
      )`,
    [surface, subject.userId, subject.email]
  );
  return { count: result.rowCount };
}

async function loadDeliveriesForEvent(client, eventId) {
  const result = await client.query(
    `select
      d.*,
      e.type as event_type,
      e.source_service,
      e.payload as event_payload
    from notification_deliveries d
    join notification_events e on e.id = d.event_id
    where d.event_id = $1
    order by d.created_at asc`,
    [eventId]
  );
  return result.rows.map(toDelivery);
}

async function loadDelivery(deliveryId) {
  const result = await pool.query(
    `select
      d.*,
      e.type as event_type,
      e.source_service,
      e.payload as event_payload
    from notification_deliveries d
    join notification_events e on e.id = d.event_id
    where d.id = $1`,
    [deliveryId]
  );
  return result.rows[0] ? toDelivery(result.rows[0]) : null;
}

async function loadDeliveryForSubject({ subject, deliveryId }) {
  const result = await pool.query(
    `select
      d.*,
      e.type as event_type,
      e.source_service,
      e.payload as event_payload
    from notification_deliveries d
    join notification_events e on e.id = d.event_id
    where d.id = $1
      and (
        ($2::text is not null and d.recipient_user_id = $2)
        or ($3::text is not null and lower(d.recipient_email) = lower($3))
      )`,
    [deliveryId, subject.userId, subject.email]
  );
  return result.rows[0] ? toDelivery(result.rows[0]) : null;
}

async function migrate() {
  await pool.query(`
    create table if not exists notification_events (
      id uuid primary key,
      type text not null,
      source_service text not null,
      actor_user_id text,
      actor_email text,
      subject_type text,
      subject_id text,
      payload jsonb not null default '{}'::jsonb,
      idempotency_key text unique,
      created_at timestamptz not null default now()
    )
  `);

  await pool.query(`
    create table if not exists notification_deliveries (
      id uuid primary key,
      event_id uuid not null references notification_events(id) on delete cascade,
      recipient_user_id text,
      recipient_email text,
      surface text not null,
      title text not null,
      body text not null default '',
      meta jsonb not null default '{}'::jsonb,
      status text not null default 'unread',
      actionable boolean not null default false,
      action_type text,
      action_url text,
      entity_type text,
      entity_id text,
      created_at timestamptz not null default now(),
      updated_at timestamptz not null default now(),
      read_at timestamptz,
      constraint notification_deliveries_recipient_check
        check (recipient_user_id is not null or recipient_email is not null),
      constraint notification_deliveries_status_check
        check (status in ('unread', 'read', 'archived'))
    )
  `);

  await pool.query("create index if not exists notification_deliveries_recipient_user_idx on notification_deliveries(recipient_user_id, surface, status, created_at desc)");
  await pool.query("create index if not exists notification_deliveries_recipient_email_idx on notification_deliveries(lower(recipient_email), surface, status, created_at desc)");
  await pool.query("create index if not exists notification_events_type_idx on notification_events(type, created_at desc)");
}

function sanitizeEventCommand(payload) {
  const type = requireNonEmptyString(payload?.type, "type");
  const sourceService = normalizeKey(payload?.sourceService || payload?.source_service || "platform");
  const deliveries = Array.isArray(payload?.deliveries) ? payload.deliveries.map(sanitizeDeliveryCommand).filter(Boolean) : [];

  if (deliveries.length === 0) {
    throw badRequest("deliveries_required");
  }

  return {
    type,
    sourceService,
    actor: sanitizeActor(payload?.actor),
    subject: sanitizeSubject(payload?.subject),
    payload: isPlainObject(payload?.payload) ? payload.payload : {},
    deliveries,
    idempotencyKey: optionalString(payload?.idempotencyKey || payload?.idempotency_key),
  };
}

function sanitizeDeliveryCommand(payload) {
  const recipientUserId = optionalString(payload?.recipientUserId || payload?.recipient_user_id);
  const recipientEmail = normalizeEmail(payload?.recipientEmail || payload?.recipient_email);

  if (!recipientUserId && !recipientEmail) {
    return null;
  }

  return {
    recipientUserId,
    recipientEmail,
    surface: sanitizeSurface(payload?.surface || "hub"),
    title: requireNonEmptyString(payload?.title, "delivery.title"),
    body: optionalString(payload?.body) || "",
    meta: isPlainObject(payload?.meta) ? payload.meta : {},
    status: sanitizeStatus(payload?.status || "unread"),
    actionable: payload?.actionable === true,
    actionType: optionalString(payload?.actionType || payload?.action_type),
    actionUrl: optionalString(payload?.actionUrl || payload?.action_url),
    entityType: optionalString(payload?.entityType || payload?.entity_type),
    entityId: optionalString(payload?.entityId || payload?.entity_id),
  };
}

function sanitizeActor(payload) {
  const actor = isPlainObject(payload) ? payload : {};
  return {
    userId: optionalString(actor.userId || actor.user_id),
    email: normalizeEmail(actor.email),
  };
}

function sanitizeSubject(payload) {
  const subject = isPlainObject(payload) ? payload : {};
  return {
    type: optionalString(subject.type),
    id: optionalString(subject.id),
  };
}

function getRequestSubject(req) {
  const userId = optionalString(req.headers["x-nodedc-user-id"] || req.query.userId);
  const email = normalizeEmail(req.headers["x-nodedc-user-email"] || req.query.email);
  if (!userId && !email) {
    throw badRequest("notification_subject_required");
  }
  return { userId, email };
}

function toEvent(row) {
  return {
    id: row.id,
    type: row.type,
    sourceService: row.source_service,
    actorUserId: row.actor_user_id,
    actorEmail: row.actor_email,
    subjectType: row.subject_type,
    subjectId: row.subject_id,
    payload: row.payload || {},
    idempotencyKey: row.idempotency_key,
    createdAt: toIso(row.created_at),
  };
}

function toDelivery(row) {
  return {
    id: row.id,
    eventId: row.event_id,
    eventType: row.event_type,
    sourceService: row.source_service,
    recipientUserId: row.recipient_user_id,
    recipientEmail: row.recipient_email,
    surface: row.surface,
    title: row.title,
    body: row.body,
    meta: row.meta || {},
    status: row.status,
    unread: row.status === "unread",
    actionable: row.actionable,
    actionType: row.action_type,
    actionUrl: row.action_url,
    entityType: row.entity_type,
    entityId: row.entity_id,
    eventPayload: row.event_payload || {},
    createdAt: toIso(row.created_at),
    updatedAt: toIso(row.updated_at),
    readAt: toIso(row.read_at),
  };
}

function broadcastDeliveryUpdate(eventName, delivery) {
  for (const client of streamClients) {
    if (!deliveryMatchesClient(delivery, client)) continue;
    writeSse(client.res, eventName, { delivery });
  }
}

function broadcastSubjectUpdate(eventName, payload) {
  for (const client of streamClients) {
    if (client.surface !== payload.surface) continue;
    if (!subjectsMatch(client.subject, payload.subject)) continue;
    writeSse(client.res, eventName, payload);
  }
}

function deliveryMatchesClient(delivery, client) {
  if (delivery.surface !== client.surface) return false;
  return subjectsMatch(client.subject, {
    userId: delivery.recipientUserId,
    email: delivery.recipientEmail,
  });
}

function subjectsMatch(left, right) {
  return Boolean(
    (left.userId && right.userId && left.userId === right.userId) ||
    (left.email && right.email && left.email.toLowerCase() === right.email.toLowerCase())
  );
}

function writeSse(res, event, data) {
  res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
}

function requireInternalApi(req, res, next) {
  if (!config.internalAccessToken) {
    res.status(503).json({ ok: false, error: "notification_core_internal_token_not_configured" });
    return;
  }

  const authorization = typeof req.headers.authorization === "string" ? req.headers.authorization : "";
  const bearerToken = authorization.match(/^Bearer\s+(.+)$/i)?.[1] || "";
  const headerToken = typeof req.headers["x-nodedc-internal-token"] === "string" ? req.headers["x-nodedc-internal-token"] : "";
  const requestToken = bearerToken || headerToken;

  if (!safeTokenEquals(requestToken, config.internalAccessToken)) {
    res.status(401).json({ ok: false, error: "notification_core_unauthorized" });
    return;
  }

  next();
}

function safeTokenEquals(actual, expected) {
  if (!actual || !expected) return false;
  const actualBuffer = Buffer.from(actual);
  const expectedBuffer = Buffer.from(expected);
  if (actualBuffer.length !== expectedBuffer.length) return false;
  return timingSafeEqual(actualBuffer, expectedBuffer);
}

function sanitizeSurface(value) {
  const surface = normalizeKey(value || "hub");
  if (!["hub", "engine", "operational-core"].includes(surface)) {
    throw badRequest("unsupported_notification_surface");
  }
  return surface;
}

function sanitizeStatus(value) {
  const status = normalizeKey(value || "unread");
  if (!["unread", "read", "archived"].includes(status)) {
    throw badRequest("unsupported_notification_status");
  }
  return status;
}

function sanitizeLimit(value) {
  const limit = Number(value || 100);
  if (!Number.isFinite(limit)) return 100;
  return Math.min(Math.max(Math.trunc(limit), 1), 200);
}

function sanitizeUuid(value, name) {
  const text = optionalString(value);
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(text || "")) {
    throw badRequest(`${name}_invalid`);
  }
  return text;
}

function requireNonEmptyString(value, name) {
  const text = optionalString(value);
  if (!text) throw badRequest(`${name}_required`);
  return text;
}

function optionalString(value) {
  if (typeof value !== "string") return null;
  const text = value.trim();
  return text ? text : null;
}

function normalizeEmail(value) {
  const text = optionalString(value);
  return text ? text.toLowerCase() : null;
}

function normalizeKey(value) {
  return String(value || "").trim().toLowerCase().replace(/_/g, "-");
}

function isPlainObject(value) {
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

function toIso(value) {
  if (!value) return null;
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}

function badRequest(message) {
  const error = new Error(message);
  error.status = 400;
  return error;
}

function asyncRoute(handler) {
  return (req, res, next) => {
    Promise.resolve(handler(req, res, next)).catch(next);
  };
}

function readConfig() {
  const databaseUrl =
    process.env.DATABASE_URL ||
    process.env.NOTIFICATION_DATABASE_URL ||
    "postgres://nodedc_notifications:nodedc_notifications@localhost:5432/nodedc_notifications";

  return {
    port: Number(process.env.PORT || process.env.NOTIFICATION_CORE_PORT || "5185"),
    databaseUrl,
    databasePoolSize: Number(process.env.NOTIFICATION_DATABASE_POOL_SIZE || "10"),
    internalAccessToken:
      process.env.NODEDC_INTERNAL_ACCESS_TOKEN ||
      process.env.NODEDC_PLATFORM_SERVICE_TOKEN ||
      "",
  };
}

async function shutdown() {
  try {
    httpServer.close();
    await pool.end();
  } finally {
    process.exit(0);
  }
}
