'use strict';

const relayStore = require('./remote-relay-store');

const REPLAY_WINDOW_MS = 5 * 60 * 1000;

// AUTHORIZATION LAYERING (not a duplicate of lib/auth-rules.js).
//
// This is the LOCAL envelope relay (`POST /api/remote/messages`) that the phone
// uses for typed, idempotent message delivery with the durable submission ledger
// (ctm_remote_submissions). It is active and current — distinct from the
// deprecated *hosted* "Walle Relay" product in walle-relay-phone-access-design.md.
//
// Authorization is intentionally two-layered, at two different granularities:
//   1. lib/auth-rules.js gates the `/api/remote/messages` HTTP ROUTE
//      (scope=respond) — the transport-level gate every request passes.
//   2. MESSAGE_REGISTRY below gates each ENVELOPE MESSAGE TYPE (scope, risk,
//      step-up, enabled) — finer-grained policy for the action the envelope
//      carries (e.g. approval.respond requires step-up; high-risk admin types
//      are disabled). The route gate cannot express per-message-type policy, so
//      this layer is additive defense-in-depth, not a second copy of the same
//      rules.
// Keep both in sync conceptually: a message type that mutates session/agent
// state must require step-up here just as its WS/HTTP equivalent does in
// auth-rules.js.
const MESSAGE_REGISTRY = Object.freeze({
  'instance.status': rule('read', 'low', false),
  'sessions.list': rule('read', 'low', false),
  'sessions.get': rule('read', 'low', false),
  'sessions.subscribe': rule('read', 'low', false, { enabled: false }),
  'wall_e.status': rule('read', 'low', false),

  'session.send_message': rule('respond', 'low', false),
  'session.submit_input': rule('respond', 'medium', false),
  'session.cancel_stream': rule('respond', 'medium', false),
  'session.prompt_history': rule('respond', 'medium', false),
  'session.select_choice': rule('respond', 'medium', false),
  'wall_e.send_message': rule('respond', 'low', false),
  'wall_e.set_model': rule('respond', 'low', false),

  'approval.respond': rule('respond', 'high', true),
  'session.kill': rule('admin', 'high', true, { enabled: false }),
  'session.close': rule('admin', 'high', true, { enabled: false }),
  'network.setup_change': rule('admin', 'high', true, { enabled: false }),
  'ctm.restart': rule('admin', 'high', false, { enabled: false }),
});

function rule(scope, risk, stepUp, extra = {}) {
  return Object.freeze({
    scope,
    risk,
    stepUp: !!stepUp,
    enabled: extra.enabled !== false,
  });
}

function publicRegistry() {
  return Object.keys(MESSAGE_REGISTRY).sort().map((type) => ({
    type,
    ...MESSAGE_REGISTRY[type],
  }));
}

function scopeSet(auth) {
  if (auth?.scopes instanceof Set) return auth.scopes;
  return new Set(Array.isArray(auth?.scopes) ? auth.scopes : []);
}

function hasScope(auth, required) {
  const scopes = scopeSet(auth);
  return scopes.has('admin') || scopes.has(required);
}

function safeError(code, message, extra = {}) {
  return { ok: false, error: code, message: message || code, ...extra };
}

function normalizeEnvelope(raw) {
  const message = raw && typeof raw === 'object' ? raw : {};
  return {
    id: String(message.id || ''),
    type: String(message.type || ''),
    version: Number(message.version || 1),
    account_id: String(message.account_id || message.accountId || ''),
    instance_id: String(message.instance_id || message.instanceId || ''),
    device_id: String(message.device_id || message.deviceId || ''),
    created_at: message.created_at || message.createdAt || '',
    body: message.body && typeof message.body === 'object' ? message.body : {},
  };
}

function createdAtMs(value, now = Date.now()) {
  if (!value) return now;
  const parsed = Date.parse(value);
  if (Number.isFinite(parsed)) return parsed;
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : 0;
}

function validateEnvelope(message, now = Date.now()) {
  if (!message.id || message.id.length > 160) return safeError('invalid_message_id');
  if (!message.type || !MESSAGE_REGISTRY[message.type]) return safeError('unknown_message_type');
  if (message.version !== 1) return safeError('unsupported_message_version');
  const created = createdAtMs(message.created_at, now);
  if (!created || Math.abs(now - created) > REPLAY_WINDOW_MS) {
    return safeError('message_replay_window_expired');
  }
  return { ok: true, created_at_ms: created };
}

function authDecision(message, ruleConfig, auth) {
  if (!auth?.authenticated) return safeError('unauthorized');
  if (!auth.isLoopback) {
    if (!auth.deviceId) return safeError('device_token_required');
    if (message.device_id && message.device_id !== auth.deviceId) return safeError('device_mismatch');
  }
  if (!hasScope(auth, ruleConfig.scope)) return safeError('insufficient_scope');
  if (ruleConfig.stepUp && !auth.isLoopback && !auth.stepUpFresh) return safeError('step_up_required');
  if (!ruleConfig.enabled) return safeError('remote_action_not_enabled');
  return { ok: true };
}

function sessionIdFromBody(body) {
  return String(body.session_id || body.sessionId || body.id || '');
}

function promptHistoryDirection(body) {
  const direction = String(body.direction || body.history || '').toLowerCase();
  if (direction === 'previous' || direction === 'prev' || direction === 'up') return 'previous';
  if (direction === 'next' || direction === 'down') return 'next';
  return '';
}

async function dispatchMessage(message, context = {}) {
  const handlers = context.handlers || {};
  switch (message.type) {
    case 'instance.status':
      return handlers.instanceStatus ? handlers.instanceStatus(message.body) : { ok: true };
    case 'sessions.list':
      return handlers.listSessions ? handlers.listSessions(message.body) : { ok: true, sessions: [] };
    case 'sessions.get':
      return handlers.getSession ? handlers.getSession(sessionIdFromBody(message.body), message.body) : safeError('handler_missing');
    case 'wall_e.status':
      return handlers.wallEStatus ? handlers.wallEStatus(message.body) : { ok: true, status: 'unknown' };
    case 'session.send_message':
      return handlers.sendSessionMessage
        ? handlers.sendSessionMessage(sessionIdFromBody(message.body), String(message.body.text || message.body.message || ''), message.body, message)
        : safeError('handler_missing');
    case 'session.submit_input':
      return handlers.submitSessionInput
        ? handlers.submitSessionInput(sessionIdFromBody(message.body), message.body, message)
        : safeError('handler_missing');
    case 'session.cancel_stream':
      return handlers.cancelSessionStream
        ? handlers.cancelSessionStream(sessionIdFromBody(message.body), message.body)
        : safeError('handler_missing');
    case 'session.prompt_history': {
      const direction = promptHistoryDirection(message.body);
      if (!direction) return safeError('invalid_prompt_history_direction');
      return handlers.navigatePromptHistory
        ? handlers.navigatePromptHistory(sessionIdFromBody(message.body), direction, message.body)
        : safeError('handler_missing');
    }
    case 'session.select_choice':
      return handlers.selectChoice
        ? handlers.selectChoice(sessionIdFromBody(message.body), message.body, message)
        : safeError('handler_missing');
    case 'wall_e.send_message':
      return handlers.sendWalleMessage
        ? handlers.sendWalleMessage(sessionIdFromBody(message.body), String(message.body.text || message.body.message || ''), message.body, message)
        : safeError('handler_missing');
    case 'wall_e.set_model':
      return handlers.setWalleModel
        ? handlers.setWalleModel(sessionIdFromBody(message.body), message.body)
        : safeError('handler_missing');
    case 'approval.respond':
      return handlers.respondApproval
        ? handlers.respondApproval(sessionIdFromBody(message.body), String(message.body.decision || message.body.action || ''), message.body, message)
        : safeError('handler_missing');
    default:
      return safeError('remote_action_not_enabled');
  }
}

async function handleRemoteMessage(db, rawMessage, context = {}) {
  relayStore.ensureRemoteRelaySchema(db);
  const message = normalizeEnvelope(rawMessage);
  const now = Number(context.now || Date.now());
  const ruleConfig = MESSAGE_REGISTRY[message.type];
  const validation = validateEnvelope(message, now);
  if (!validation.ok) return validation;

  const duplicate = relayStore.getIdempotentResult(db, message.id);
  if (duplicate) return { ...duplicate.result, duplicate: true };

  const auth = context.auth || {};
  const decision = authDecision(message, ruleConfig, auth);
  if (!decision.ok) {
    relayStore.insertAudit(db, {
      accountId: message.account_id,
      instanceId: message.instance_id,
      deviceId: auth.deviceId || message.device_id,
      sessionId: sessionIdFromBody(message.body),
      action: message.type,
      risk: ruleConfig.risk,
      result: decision.error,
      metadata: { message_id: message.id },
    });
    return decision;
  }

  let result;
  try {
    result = await dispatchMessage(message, context);
  } catch (err) {
    result = safeError('handler_failed', err && err.message ? err.message : 'handler_failed');
  }
  const finalResult = {
    ok: !!result?.ok,
    type: message.type,
    result,
  };
  relayStore.insertAudit(db, {
    accountId: message.account_id,
    instanceId: message.instance_id,
    deviceId: auth.deviceId || message.device_id,
    sessionId: sessionIdFromBody(message.body),
    action: message.type,
    risk: ruleConfig.risk,
    result: finalResult.ok ? 'allow' : (result?.error || 'deny'),
    metadata: { message_id: message.id },
  });
  relayStore.storeIdempotentResult(db, message, finalResult, now);
  return finalResult;
}

module.exports = {
  MESSAGE_REGISTRY,
  handleRemoteMessage,
  normalizeEnvelope,
  publicRegistry,
  validateEnvelope,
};
