export const ENVIRONMENT_DEV = "dev";
export const ENVIRONMENT_V02 = "v02";
export const PROTOCOL_ENVIRONMENTS = Object.freeze([ENVIRONMENT_DEV, ENVIRONMENT_V02]);
export const DEV_ENDPOINT = "http://74.48.78.17:16667";
export const DEV_FRONTEND_ENDPOINT = "http://74.48.78.17:16666";
export const JSON_RPC_VERSION = "2.0";

export const SERVICE_IDS = Object.freeze([
  "hwlab-cloud-api",
  "hwlab-cloud-web",
  "hwlab-gateway",
  "hwlab-edge-proxy",
  "hwlab-agent-skills"
]);

export const TABLES = Object.freeze([
  "projects",
  "users",
  "user_sessions",
  "gateway_sessions",
  "box_resources",
  "box_capabilities",
  "wiring_configs",
  "patch_panel_status",
  "hardware_operations",
  "audit_events",
  "agent_sessions",
  "account_workspaces",
  "worker_sessions",
  "agent_trace_events",
  "evidence_records"
]);

export const ERROR_CODES = Object.freeze({
  parseError: -32700,
  invalidRequest: -32600,
  methodNotFound: -32601,
  invalidParams: -32602,
  internalError: -32603,
  hwlabUnknown: -32000,
  projectNotFound: -32010,
  sessionNotFound: -32011,
  capabilityUnavailable: -32020,
  resourceLocked: -32021,
  wiringInvalid: -32022,
  operationRejected: -32023,
  evidenceMissing: -32030,
  auditRequired: -32040
});

function freezeRecordShape({ required, allowed }) {
  return Object.freeze({
    required: Object.freeze([...required]),
    allowed: Object.freeze([...allowed])
  });
}

export const PROTOCOL_RECORD_SHAPES = Object.freeze({
  gatewaySession: freezeRecordShape({
    required: [
      "gatewaySessionId",
      "projectId",
      "serviceId",
      "gatewayId",
      "status",
      "environment",
      "startedAt"
    ],
    allowed: [
      "gatewaySessionId",
      "projectId",
      "serviceId",
      "gatewayId",
      "endpoint",
      "status",
      "environment",
      "startedAt",
      "lastSeenAt",
      "stoppedAt",
      "labels"
    ]
  }),
  boxResource: freezeRecordShape({
    required: [
      "resourceId",
      "projectId",
      "gatewaySessionId",
      "boxId",
      "resourceType",
      "state",
      "environment",
      "createdAt",
      "updatedAt"
    ],
    allowed: [
      "resourceId",
      "projectId",
      "gatewaySessionId",
      "boxId",
      "resourceType",
      "name",
      "state",
      "environment",
      "metadata",
      "createdAt",
      "updatedAt"
    ]
  }),
  boxCapability: freezeRecordShape({
    required: [
      "capabilityId",
      "resourceId",
      "projectId",
      "name",
      "direction",
      "valueType",
      "mutatesState",
      "createdAt",
      "updatedAt"
    ],
    allowed: [
      "capabilityId",
      "resourceId",
      "projectId",
      "name",
      "description",
      "direction",
      "valueType",
      "unit",
      "constraints",
      "mutatesState",
      "createdAt",
      "updatedAt"
    ]
  }),
  hardwareOperation: freezeRecordShape({
    required: [
      "operationId",
      "projectId",
      "gatewaySessionId",
      "resourceId",
      "capabilityId",
      "requestedBy",
      "status",
      "environment",
      "requestedAt",
      "updatedAt"
    ],
    allowed: [
      "operationId",
      "projectId",
      "gatewaySessionId",
      "agentSessionId",
      "workerSessionId",
      "resourceId",
      "capabilityId",
      "requestedBy",
      "input",
      "output",
      "status",
      "environment",
      "requestedAt",
      "startedAt",
      "completedAt",
      "updatedAt",
      "error"
    ]
  }),
  auditEvent: freezeRecordShape({
    required: [
      "auditId",
      "traceId",
      "actorType",
      "actorId",
      "action",
      "targetType",
      "targetId",
      "serviceId",
      "environment",
      "occurredAt"
    ],
    allowed: [
      "auditId",
      "traceId",
      "actorType",
      "actorId",
      "action",
      "targetType",
      "targetId",
      "projectId",
      "gatewaySessionId",
      "workerSessionId",
      "operationId",
      "serviceId",
      "environment",
      "outcome",
      "reason",
      "metadata",
      "occurredAt"
    ]
  }),
  evidenceRecord: freezeRecordShape({
    required: [
      "evidenceId",
      "projectId",
      "operationId",
      "kind",
      "uri",
      "sha256",
      "serviceId",
      "environment",
      "createdAt"
    ],
    allowed: [
      "evidenceId",
      "projectId",
      "operationId",
      "agentSessionId",
      "workerSessionId",
      "kind",
      "uri",
      "mimeType",
      "sha256",
      "sizeBytes",
      "serviceId",
      "environment",
      "metadata",
      "createdAt"
    ]
  })
});

export class HwlabProtocolError extends Error {
  constructor(message, { code = ERROR_CODES.hwlabUnknown, data = {}, context = {} } = {}) {
    super(message);
    this.name = "HwlabProtocolError";
    this.code = code;
    this.data = data;
    this.context = context;
  }
}

const methodPattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$/;
const protocolIdPattern = /^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/;
const actionPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
const timestampFieldNames = new Set([
  "startedAt",
  "lastSeenAt",
  "stoppedAt",
  "createdAt",
  "updatedAt",
  "requestedAt",
  "completedAt",
  "occurredAt"
]);
const idFieldNames = new Set([
  "gatewaySessionId",
  "projectId",
  "gatewayId",
  "resourceId",
  "boxId",
  "capabilityId",
  "operationId",
  "agentSessionId",
  "workerSessionId",
  "requestedBy",
  "auditId",
  "traceId",
  "actorId",
  "targetId",
  "evidenceId"
]);
const actorTypes = new Set(["user", "service", "agent", "worker", "system"]);
const auditOutcomes = new Set(["accepted", "succeeded", "failed", "rejected", "canceled"]);
const evidenceKinds = new Set(["log", "screenshot", "measurement", "artifact", "report", "trace"]);
const gatewayStatuses = new Set(["starting", "connected", "degraded", "disconnected", "stopped"]);
const resourceStates = new Set(["available", "reserved", "running", "maintenance", "offline"]);
const operationStatuses = new Set([
  "requested",
  "accepted",
  "queued",
  "running",
  "succeeded",
  "failed",
  "rejected",
  "canceled",
  "timed_out"
]);
const resourceTypes = new Set([
  "board",
  "power_supply",
  "serial_port",
  "network_port",
  "relay",
  "sensor",
  "simulator_endpoint"
]);
const capabilityDirections = new Set(["input", "output", "bidirectional"]);
const capabilityValueTypes = new Set(["boolean", "integer", "number", "string", "object", "binary"]);

export function isFrozenServiceId(serviceId) {
  return SERVICE_IDS.includes(serviceId);
}

export function isProtocolEnvironment(environment) {
  return PROTOCOL_ENVIRONMENTS.includes(environment);
}

export function isProtocolId(value) {
  return typeof value === "string" && protocolIdPattern.test(value);
}

export function validateMeta(meta) {
  if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
    throw new Error("meta must be an object");
  }
  if (!meta.traceId) {
    throw new Error("meta.traceId is required");
  }
  if (!isFrozenServiceId(meta.serviceId)) {
    throw new Error(`unknown serviceId ${JSON.stringify(meta.serviceId)}`);
  }
  if (!isProtocolEnvironment(meta.environment)) {
    throw new Error(`unsupported environment ${JSON.stringify(meta.environment)}`);
  }
}

export function validateRequest(envelope) {
  if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
    throw new Error("request must be an object");
  }
  if (envelope.jsonrpc !== JSON_RPC_VERSION) {
    throw new Error(`jsonrpc must be ${JSON_RPC_VERSION}`);
  }
  if (envelope.id === undefined || envelope.id === null) {
    throw new Error("id is required");
  }
  if (!methodPattern.test(envelope.method)) {
    throw new Error(`invalid method ${JSON.stringify(envelope.method)}`);
  }
  if (
    envelope.params !== undefined &&
    (!envelope.params || typeof envelope.params !== "object" || Array.isArray(envelope.params))
  ) {
    throw new Error("params must be an object when present");
  }
  validateMeta(envelope.meta);
}

export function validateResponse(envelope) {
  if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
    throw new Error("response must be an object");
  }
  if (envelope.jsonrpc !== JSON_RPC_VERSION) {
    throw new Error(`jsonrpc must be ${JSON_RPC_VERSION}`);
  }
  if (envelope.id === undefined || envelope.id === null) {
    throw new Error("id is required");
  }
  const hasResult = Object.hasOwn(envelope, "result");
  const hasError = Object.hasOwn(envelope, "error");
  if (hasResult === hasError) {
    throw new Error("response must contain exactly one of result or error");
  }
  if (hasError) {
    validateRpcError(envelope.error);
  }
  validateMeta(envelope.meta);
}

export function validateRpcError(error) {
  if (!error || typeof error !== "object" || Array.isArray(error)) {
    throw new Error("error must be an object");
  }
  if (!Number.isInteger(error.code)) {
    throw new Error("error.code must be an integer");
  }
  if (!error.message || typeof error.message !== "string") {
    throw new Error("error.message is required");
  }
}

export function errorNameForCode(code) {
  for (const [name, value] of Object.entries(ERROR_CODES)) {
    if (value === code) {
      return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
    }
  }

  return "hwlab_unknown";
}

export function createRpcErrorBody({ code = ERROR_CODES.hwlabUnknown, message = "HWLAB runtime error", data = {} }) {
  const error = {
    code,
    name: errorNameForCode(code),
    message,
    data
  };

  validateRpcError(error);
  return {
    error
  };
}

export function assertProtocolRecord(kind, record) {
  const shape = PROTOCOL_RECORD_SHAPES[kind];
  if (!shape) {
    throw new Error(`unknown protocol record kind ${JSON.stringify(kind)}`);
  }
  if (!record || typeof record !== "object" || Array.isArray(record)) {
    throw new Error(`${kind} must be an object`);
  }

  for (const field of shape.required) {
    if (!Object.hasOwn(record, field)) {
      throw new Error(`${kind}.${field} is required`);
    }
  }

  const allowed = new Set(shape.allowed);
  for (const [field, value] of Object.entries(record)) {
    if (!allowed.has(field)) {
      throw new Error(`${kind}.${field} is not part of the frozen protocol schema`);
    }
    assertProtocolField(kind, field, value);
  }
}

export function assertProtocolRecords(kind, records) {
  if (!Array.isArray(records)) {
    throw new Error(`${kind} records must be an array`);
  }
  for (const record of records) {
    assertProtocolRecord(kind, record);
  }
}

function assertProtocolField(kind, field, value) {
  if (idFieldNames.has(field) && !isProtocolId(value)) {
    throw new Error(`${kind}.${field} must be a protocol id`);
  }
  if (timestampFieldNames.has(field) && Number.isNaN(Date.parse(value))) {
    throw new Error(`${kind}.${field} must be an RFC 3339 timestamp`);
  }
  if (field === "environment" && !isProtocolEnvironment(value)) {
    throw new Error(`${kind}.environment must be one of ${PROTOCOL_ENVIRONMENTS.join(", ")}`);
  }
  if (field === "serviceId" && !isFrozenServiceId(value)) {
    throw new Error(`${kind}.serviceId is not a frozen service id`);
  }
  if ((field === "action" || field === "name") && !actionPattern.test(value)) {
    throw new Error(`${kind}.${field} must be a dotted lower-case action name`);
  }
  if (field === "actorType" && !actorTypes.has(value)) {
    throw new Error(`${kind}.actorType is invalid`);
  }
  if (field === "outcome" && !auditOutcomes.has(value)) {
    throw new Error(`${kind}.outcome is invalid`);
  }
  if (field === "kind" && !evidenceKinds.has(value)) {
    throw new Error(`${kind}.kind is invalid`);
  }
  if (field === "status") {
    assertStatusField(kind, value);
  }
  if (field === "state" && !resourceStates.has(value)) {
    throw new Error(`${kind}.state is invalid`);
  }
  if (field === "resourceType" && !resourceTypes.has(value)) {
    throw new Error(`${kind}.resourceType is invalid`);
  }
  if (field === "direction" && !capabilityDirections.has(value)) {
    throw new Error(`${kind}.direction is invalid`);
  }
  if (field === "valueType" && !capabilityValueTypes.has(value)) {
    throw new Error(`${kind}.valueType is invalid`);
  }
  if (field === "mutatesState" && typeof value !== "boolean") {
    throw new Error(`${kind}.mutatesState must be boolean`);
  }
  if (field === "sha256" && !/^[a-f0-9]{64}$/.test(value)) {
    throw new Error(`${kind}.sha256 must be lowercase sha256 hex`);
  }
}

function assertStatusField(kind, value) {
  if (kind === "gatewaySession" && !gatewayStatuses.has(value)) {
    throw new Error(`${kind}.status is invalid`);
  }
  if (kind === "boxResource" && !resourceStates.has(value)) {
    throw new Error(`${kind}.status is invalid`);
  }
  if (kind === "hardwareOperation" && !operationStatuses.has(value)) {
    throw new Error(`${kind}.status is invalid`);
  }
}
