import { createHash, randomBytes } from 'crypto';
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { connect, createServer, type Server, type Socket } from 'net';
import type { AuthProfile } from '../lib/auth.js';
import { getProfileRuntimeStateDir } from '../lib/init-command.js';
import type { TelemetryRecorder } from './observability/index.js';

export const GAME_START_RUNTIME_FILE = 'game-start.json';
export const OWNER_CONTROL_TIMEOUT_MS = 2000;
const MAX_CONTROL_REQUEST_BYTES = 1024 * 1024;
const MAX_CONTROL_CONNECTIONS = 64;
// Capture protocol primitives so runtime modules cannot rewrite authenticated
// owner-control requests by monkey-patching global JSON.
const SAFE_JSON_PARSE = JSON.parse.bind(JSON) as typeof JSON.parse;
const SAFE_JSON_STRINGIFY = JSON.stringify.bind(JSON) as typeof JSON.stringify;

export type MasterOwnerControlType =
  | 'quit'
  | 'leave'
  | 'stop'
  | 'training_mode'
  | 'training_role_lock'
  | 'training_role_clear'
  | 'switch_strategy'
  | 'update_strategy_control'
  | 'snapshot'
  | 'action'
  | 'mailbox_claim'
  | 'mailbox_ack'
  | 'mailbox_nack'
  | 'mailbox_events_prepare'
  | 'mailbox_events_ack'
  | 'mailbox_events_release';

export type HostRelayOwnerControlType =
  | 'mailbox_claim'
  | 'mailbox_ack'
  | 'mailbox_nack';

export type OwnerControlType = MasterOwnerControlType;

export interface OwnerControlInfo {
  kind: 'node-net-socket';
  path: string;
}

export interface OwnerControlRequest {
  token?: string;
  relay_token?: string;
  type?: OwnerControlType;
  strategy?: string;
  args?: string[];
  [key: string]: any;
}

export interface OwnerControlResponse {
  ok: boolean;
  type?: string;
  error?: string;
  message?: string;
  [key: string]: any;
}

export type OwnerControlRequestContext =
  | { principal: 'master' }
  | { principal: 'host_relay' };

export interface OwnerControlServerOptions {
  observability?: TelemetryRecorder;
}

export interface OwnerControlServer {
  control: OwnerControlInfo;
  /** Master authority for short CLI commands only. Never pass this to workers. */
  token: string;
  /** Narrow Mailbox-only authority for disposable Host Relays. */
  relayToken: string;
  close: () => Promise<void>;
}

export function gameStartRuntimePath(stateDir: string): string {
  return join(stateDir, GAME_START_RUNTIME_FILE);
}

export function readGameStartRuntime(stateDir: string): Record<string, any> | null {
  const runtimePath = gameStartRuntimePath(stateDir);
  if (!existsSync(runtimePath)) return null;
  try {
    return SAFE_JSON_PARSE(readFileSync(runtimePath, 'utf8'));
  } catch {}
  return null;
}

function ownerControlPath(stateDir: string, pid: number, nonce: string): string {
  const hash = createHash('sha1').update(stateDir).digest('hex').slice(0, 12);
  if (process.platform === 'win32') return `\\\\.\\pipe\\clawclaw-${hash}-${pid}-${nonce}`;
  const userId = typeof process.getuid === 'function' ? String(process.getuid()) : hash;
  const dir = join(tmpdir(), `clawclaw-${userId}`);
  mkdirSync(dir, { recursive: true, mode: 0o700 });
  try { chmodSync(dir, 0o700); } catch {}
  return join(dir, `${hash}-${pid}-${nonce}.sock`);
}

function safeRequest(request: OwnerControlRequest): OwnerControlRequest {
  const {
    token: _token,
    relay_token: _relayToken,
    ...rest
  } = request;
  return rest;
}

function sendLine(socket: import('net').Socket, value: OwnerControlResponse): void {
  socket.end(`${SAFE_JSON_STRINGIFY(value)}\n`);
}

export async function startOwnerControlServer(
  stateDir: string,
  onRequest: (
    request: OwnerControlRequest,
    context: OwnerControlRequestContext,
  ) => Promise<OwnerControlResponse> | OwnerControlResponse,
  options: OwnerControlServerOptions = {},
): Promise<OwnerControlServer> {
  const token = randomBytes(24).toString('hex');
  const relayToken = randomBytes(24).toString('hex');
  const path = ownerControlPath(stateDir, process.pid, randomBytes(8).toString('hex'));
  if (process.platform !== 'win32') {
    try { unlinkSync(path); } catch {}
  }

  const sockets = new Set<Socket>();
  const server: Server = createServer((socket) => {
    sockets.add(socket);
    socket.on('error', () => {});
    socket.once('close', () => sockets.delete(socket));
    socket.setTimeout(OWNER_CONTROL_TIMEOUT_MS, () => socket.destroy());
    let buffer = '';
    let handled = false;
    const respond = (value: OwnerControlResponse): void => {
      sendLine(socket, value);
    };
    socket.setEncoding('utf8');
    socket.on('data', (chunk) => {
      if (handled) return;
      buffer += chunk;
      if (Buffer.byteLength(buffer, 'utf8') > MAX_CONTROL_REQUEST_BYTES) {
        handled = true;
        respond({ ok: false, error: 'owner_control_request_too_large' });
        return;
      }
      const nl = buffer.indexOf('\n');
      if (nl < 0) return;
      handled = true;
      socket.setTimeout(0);
      const line = buffer.slice(0, nl);
      void (async () => {
        try {
          const request = SAFE_JSON_PARSE(line) as OwnerControlRequest;
          if (request.token === token) {
            const response = await onRequest(safeRequest(request), { principal: 'master' });
            respond(response);
            return;
          }
          if (request.relay_token === relayToken) {
            if (!['mailbox_claim', 'mailbox_ack', 'mailbox_nack'].includes(String(request.type ?? ''))) {
              respond({ ok: false, error: 'host_relay_request_not_allowed' });
              return;
            }
            const response = await onRequest(safeRequest(request), { principal: 'host_relay' });
            respond(response);
            return;
          }

          respond({ ok: false, error: 'invalid_token' });
        } catch (err: any) {
          respond({ ok: false, error: err?.message ?? String(err) });
        }
      })();
    });
  });
  server.maxConnections = MAX_CONTROL_CONNECTIONS;
  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(path, () => {
      server.off('error', reject);
      resolve();
    });
  });
  if (process.platform !== 'win32') {
    try { chmodSync(path, 0o600); } catch {}
  }

  const control: OwnerControlInfo = { kind: 'node-net-socket', path };
  options.observability?.log('info', 'owner.control.started', {
    transport: control.kind,
  }, { component: 'runtime.owner_control' });
  let closed = false;
  return {
    control,
    token,
    relayToken,
    close: async () => {
      if (closed) return;
      closed = true;
      await new Promise<void>((resolve) => {
        server.close(() => resolve());
        for (const socket of sockets) socket.destroy();
      });
      if (process.platform !== 'win32') {
        try { unlinkSync(path); } catch {}
      }
      options.observability?.log('info', 'owner.control.stopped', {}, {
        component: 'runtime.owner_control',
      });
    },
  };
}

function sendControlRequest(
  path: string,
  request: OwnerControlRequest,
  timeoutMs: number,
): Promise<OwnerControlResponse | null> {
  return new Promise((resolve, reject) => {
    const socket = connect(path);
    let buffer = '';
    let settled = false;
    const finishResolve = (value: OwnerControlResponse | null): void => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve(value);
    };
    const finishReject = (error: unknown): void => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      reject(error);
    };
    const timer = setTimeout(() => {
      socket.destroy();
      finishReject(new Error('owner_control_timeout'));
    }, timeoutMs);
    socket.setEncoding('utf8');
    socket.on('connect', () => {
      socket.write(`${SAFE_JSON_STRINGIFY(request)}\n`);
    });
    socket.on('data', (chunk) => {
      buffer += chunk;
      const nl = buffer.indexOf('\n');
      if (nl < 0) return;
      socket.end();
      try {
        finishResolve(SAFE_JSON_PARSE(buffer.slice(0, nl)));
      } catch (err) {
        finishReject(err);
      }
    });
    socket.on('error', finishReject);
    socket.on('close', () => {
      if (!buffer) finishResolve(null);
    });
  });
}

/** Master/short-command path. This is the only helper that reads game-start.json. */
export function sendOwnerControlRequest(
  stateDir: string,
  type: OwnerControlType,
  payload: Record<string, any> = {},
  timeoutMs = OWNER_CONTROL_TIMEOUT_MS,
): Promise<OwnerControlResponse | null> {
  const info = readGameStartRuntime(stateDir);
  const path = typeof info?.control?.path === 'string' ? info.control.path : '';
  const token = typeof info?.control_token === 'string' ? info.control_token : '';
  if (!path || !token) return Promise.resolve(null);
  return sendControlRequest(path, { ...payload, token, type }, timeoutMs);
}

/** Narrow Host Relay path; this authority cannot execute actions or lifecycle commands. */
export function sendOwnerRelayControlRequest(
  stateDir: string,
  type: HostRelayOwnerControlType,
  payload: Record<string, any> = {},
  timeoutMs = OWNER_CONTROL_TIMEOUT_MS,
): Promise<OwnerControlResponse | null> {
  const info = readGameStartRuntime(stateDir);
  const path = typeof info?.control?.path === 'string' ? info.control.path : '';
  const relayToken = typeof info?.relay_token === 'string' ? info.relay_token : '';
  if (!path || !relayToken) return Promise.resolve(null);
  return sendControlRequest(path, {
    ...payload,
    relay_token: relayToken,
    type,
  }, timeoutMs);
}

/** Stable owner-control lookup for Agent-facing short commands. */
export function sendProfileOwnerControlRequest(
  profile: Pick<AuthProfile, 'apiKey' | 'runtimeAccountId'>,
  type: OwnerControlType,
  payload: Record<string, any> = {},
  timeoutMs = OWNER_CONTROL_TIMEOUT_MS,
): Promise<OwnerControlResponse | null> {
  return sendOwnerControlRequest(
    getProfileRuntimeStateDir(profile),
    type,
    payload,
    timeoutMs,
  );
}
