"use strict";

const crypto = require("node:crypto");
const fs = require("node:fs");

const CONNECT_SCHEMA = "guanghu.router-connect/v1";
const APPROVAL_SCHEMA = "guanghu.router-approval/v1";
const MAX_CLOCK_SKEW_SECONDS = 120;

class GuanghuRouter {
  constructor({ devices = [], challengeTtl = 60, routeTokenTtl = 30 } = {}) {
    this.devices = new Map(
      devices
        .filter(validDevice)
        .map(device => [device.device_id, Object.freeze({ ...device })]),
    );
    this.challengeTtl = Math.max(15, Number(challengeTtl) || 60);
    this.routeTokenTtl = Math.max(10, Number(routeTokenTtl) || 30);
    this.challenges = new Map();
    this.routeTokens = new Map();
    this.connections = new Map();
  }

  challenge(deviceId, now = Date.now() / 1000) {
    const device = this.devices.get(String(deviceId || ""));
    if (!device || !device.enabled) return { ok: false, reason: "device_not_registered" };
    const challengeId = crypto.randomUUID();
    const nonce = randomToken();
    this.challenges.set(challengeId, {
      deviceId: device.device_id,
      nonce,
      expiresAt: now + this.challengeTtl,
    });
    return {
      ok: true,
      schema: CONNECT_SCHEMA,
      challengeId,
      nonce,
      expiresAt: now + this.challengeTtl,
      serverTime: now,
    };
  }

  authorizeConnection(input, now = Date.now() / 1000) {
    const deviceId = String(input && input.deviceId || "");
    const challengeId = String(input && input.challengeId || "");
    const challenge = this.challenges.get(challengeId);
    const device = this.devices.get(deviceId);
    if (!device || !device.enabled) return { ok: false, reason: "device_not_registered" };
    if (!challenge || challenge.deviceId !== deviceId) return { ok: false, reason: "challenge_not_found" };
    if (now > challenge.expiresAt) {
      this.challenges.delete(challengeId);
      return { ok: false, reason: "challenge_expired" };
    }
    const clientTimestamp = Number(input && input.clientTimestamp);
    if (!Number.isFinite(clientTimestamp) || Math.abs(now - clientTimestamp) > MAX_CLOCK_SKEW_SECONDS) {
      return { ok: false, reason: "device_clock_out_of_range" };
    }
    const message = canonicalConnect({
      deviceId,
      challengeId,
      nonce: challenge.nonce,
      clientTimestamp,
    });
    if (!verifyDeviceSignature(device, message, input && input.signature)) {
      return { ok: false, reason: "device_signature_invalid" };
    }

    this.challenges.delete(challengeId);
    const routeToken = randomToken();
    this.routeTokens.set(tokenHash(routeToken), {
      deviceId,
      expiresAt: now + this.routeTokenTtl,
    });
    return {
      ok: true,
      deviceId,
      deviceLabel: device.label,
      ownerId: device.owner_id,
      routeToken,
      expiresAt: now + this.routeTokenTtl,
    };
  }

  open(routeToken, send, now = Date.now() / 1000) {
    const key = tokenHash(routeToken || "");
    const pending = this.routeTokens.get(key);
    if (!pending) return { ok: false, reason: "route_token_not_found" };
    this.routeTokens.delete(key);
    if (now > pending.expiresAt) return { ok: false, reason: "route_token_expired" };
    const device = this.devices.get(pending.deviceId);
    if (!device || !device.enabled) return { ok: false, reason: "device_not_registered" };

    const connectionId = crypto.randomUUID();
    const existing = this.connections.get(device.device_id);
    if (existing) existing.close(now, "replaced");
    const connection = {
      connectionId,
      device,
      send,
      openedAt: now,
      closed: false,
      close: (closedAt = Date.now() / 1000, reason = "client_closed") => {
        if (connection.closed) return;
        connection.closed = true;
        if (this.connections.get(device.device_id) === connection) {
          this.connections.delete(device.device_id);
        }
        send({
          type: "router.closed",
          reason,
          receipt: routeReceipt("offline", device, connectionId, closedAt),
        });
      },
    };
    this.connections.set(device.device_id, connection);
    const receipt = routeReceipt("online", device, connectionId, now);
    send({ type: "router.connected", connection_id: connectionId, receipt });
    return {
      ok: true,
      state: "online",
      connectionId,
      deviceId: device.device_id,
      ownerId: device.owner_id,
      receipt,
      close: connection.close,
    };
  }

  isApproverOnline(approverId) {
    return [...this.connections.values()].some(connection => (
      !connection.closed && connection.device.owner_id === String(approverId || "")
    ));
  }

  deliver(approverId, order) {
    const event = {
      type: "authorization.requested",
      digest: workorderDigest(order),
      workorder: order,
    };
    let delivered = 0;
    for (const connection of this.connections.values()) {
      if (!connection.closed && connection.device.owner_id === String(approverId || "")) {
        connection.send(event);
        delivered += 1;
      }
    }
    return delivered;
  }

  verifyApproval(deviceId, order, signature) {
    const connection = this.connections.get(String(deviceId || ""));
    if (!connection || connection.closed) return { ok: false, reason: "device_route_offline" };
    const digest = workorderDigest(order);
    const message = canonicalApproval({
      deviceId: connection.device.device_id,
      workorderId: order.id,
      digest,
    });
    if (!verifyDeviceSignature(connection.device, message, signature)) {
      return { ok: false, reason: "device_signature_invalid" };
    }
    return {
      ok: true,
      authorizerId: connection.device.owner_id,
      deviceId: connection.device.device_id,
      digest,
    };
  }
}

function loadDevices(file) {
  if (!file || !fs.existsSync(file)) return [];
  const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
  if (!Array.isArray(parsed)) throw new Error("HoloLake device registry must be a JSON array");
  return parsed.filter(validDevice);
}

function validDevice(device) {
  return Boolean(
    device
    && typeof device.device_id === "string"
    && /^[A-Za-z0-9._-]{3,128}$/.test(device.device_id)
    && typeof device.owner_id === "string"
    && device.owner_id.length > 0
    && typeof device.label === "string"
    && device.label.length > 0
    && typeof device.public_key === "string"
    && /^[A-Za-z0-9_-]{40,64}$/.test(device.public_key)
    && typeof device.enabled === "boolean",
  );
}

function verifyDeviceSignature(device, message, signature) {
  try {
    const publicKey = crypto.createPublicKey({
      key: { kty: "OKP", crv: "Ed25519", x: device.public_key },
      format: "jwk",
    });
    return crypto.verify(
      null,
      Buffer.from(message),
      publicKey,
      Buffer.from(String(signature || ""), "base64url"),
    );
  } catch {
    return false;
  }
}

function canonicalConnect({ deviceId, challengeId, nonce, clientTimestamp }) {
  return [
    CONNECT_SCHEMA,
    String(deviceId),
    String(challengeId),
    String(nonce),
    String(clientTimestamp),
  ].join("\n");
}

function canonicalApproval({ deviceId, workorderId, digest }) {
  return [
    APPROVAL_SCHEMA,
    String(deviceId),
    String(workorderId),
    String(digest),
  ].join("\n");
}

function workorderDigest(order) {
  const value = {
    id: String(order.id || ""),
    persona_id: String(order.persona && order.persona.pid || ""),
    persona_name: String(order.persona && order.persona.name || ""),
    target: String(order.target || ""),
    scope: String(order.scope || ""),
    action: String(order.action || ""),
    allowed_actions: [...(order.allowed_actions || order.allowedActions || [order.action])].map(String),
    description: String(order.description || ""),
    resource: String(order.resource || ""),
    created_at: Number(order.createdAt || 0),
    expires_at: Number(order.expiresAt || 0),
  };
  return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
}

function routeReceipt(state, device, connectionId, now) {
  return {
    schema: "guanghu.route-receipt/v1",
    receipt_id: crypto.randomUUID(),
    state,
    device_id: device.device_id,
    owner_id: device.owner_id,
    node_id: "JD-FD-PRIMARY",
    connection_id: connectionId,
    occurred_at: now,
  };
}

function randomToken() {
  return crypto.randomBytes(32).toString("base64url");
}

function tokenHash(value) {
  return crypto.createHash("sha256").update(String(value)).digest("hex");
}

module.exports = {
  APPROVAL_SCHEMA,
  CONNECT_SCHEMA,
  GuanghuRouter,
  canonicalApproval,
  canonicalConnect,
  loadDevices,
  workorderDigest,
};
