import { lstatSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import YAML from 'yaml';
import { canonicalizeRoleClass } from '../commands/fleet-personas.js';

interface RawFleetRoster {
  version?: unknown;
  transport?: unknown;
  tmux?: {
    socket_name?: unknown;
    socketName?: unknown;
    holder_session?: unknown;
    holderSession?: unknown;
  };
  defaults?: {
    working_directory?: unknown;
    workingDirectory?: unknown;
  };
  runtimes?: Record<string, { reset_command?: unknown; resetCommand?: unknown }>;
  agents?: Array<{
    name?: unknown;
    alias?: unknown;
    provider?: unknown;
    runtime?: unknown;
    class?: unknown;
    host?: unknown;
    ssh?: unknown;
    socket?: unknown;
    working_directory?: unknown;
    workingDirectory?: unknown;
    model_hint?: unknown;
    modelHint?: unknown;
    reasoning_level?: unknown;
    reasoningLevel?: unknown;
    tool_policy?: unknown;
    toolPolicy?: unknown;
    persistent_persona?: unknown;
    persistentPersona?: unknown;
    reset_between_tasks?: unknown;
    resetBetweenTasks?: unknown;
    kickstart_template?: unknown;
    kickstartTemplate?: unknown;
  }>;
  connector?: {
    kind?: unknown;
    matrix?: {
      homeserver_url?: unknown;
      user_id?: unknown;
      room_id?: unknown;
    };
    discord?: {
      channel_id?: unknown;
    };
  };
}

export interface FleetAgent {
  name: string;
  alias?: string;
  provider?: string;
  runtime: string;
  className: string;
  /** Resolved host identity. Absent means the caller's authoritative local host. */
  host?: string;
  /** Explicit SSH destination for a cross-host inventory peer. */
  ssh?: string;
  /** Compatibility declaration; when set it must equal fleet-wide tmux.socketName. */
  socket?: string;
  workingDirectory?: string;
  modelHint?: string;
  reasoningLevel?: string;
  toolPolicy?: string;
  persistentPersona?: boolean | string;
  resetBetweenTasks?: boolean;
  kickstartTemplate?: string;
}

export type FleetConnector =
  | { kind: 'tmux' }
  | {
      kind: 'discord';
      discord: { channelId: string };
    }
  | {
      kind: 'matrix';
      matrix: { homeserverUrl: string; userId: string; roomId: string };
    };

export interface FleetRoster {
  version: 1;
  transport: 'tmux';
  tmux: {
    socketName: string;
    holderSession: string;
  };
  defaults: {
    workingDirectory: string;
  };
  runtimes: Record<string, { resetCommand: string }>;
  agents: FleetAgent[];
  connector?: FleetConnector;
}

export type FleetRosterInputFormat = 'yaml' | 'json';

export class FleetRosterConfigurationError extends Error {
  override name = 'FleetRosterConfigurationError';
}

export function resolveInstalledFleetRosterPath(mosaicHome: string): string {
  const yamlPath = join(mosaicHome, 'fleet', 'roster.yaml');
  try {
    lstatSync(yamlPath);
    return yamlPath;
  } catch (error) {
    if (!isNodeErrorCode(error, 'ENOENT')) throw error;
    return join(mosaicHome, 'fleet', 'roster.json');
  }
}

const DEFAULT_HOLDER_SESSION = '_holder';
const DEFAULT_WORKING_DIRECTORY = '~/src';
const DEFAULT_RUNTIME_RESETS: Record<string, { resetCommand: string }> = {
  claude: { resetCommand: '/clear' },
  codex: { resetCommand: '/clear' },
  opencode: { resetCommand: '/clear' },
  pi: { resetCommand: '/new' },
};

/** One structural v1 resolver used by fleet commands and runtime comms composition. */
export function parseFleetRosterV1(
  source: string,
  format: FleetRosterInputFormat = 'yaml',
): FleetRoster {
  const trimmed = source.trim();
  const parsed =
    format === 'json'
      ? (JSON.parse(trimmed) as RawFleetRoster)
      : (YAML.parse(trimmed) as RawFleetRoster);
  return normalizeFleetRosterV1(parsed);
}

export async function loadFleetRoster(path: string): Promise<FleetRoster> {
  const source = await readFleetRosterText(path);
  try {
    return parseFleetRosterV1(source, path.endsWith('.json') ? 'json' : 'yaml');
  } catch (error) {
    if (isRosterParserError(error)) throw invalidFleetRosterError(path);
    throw error;
  }
}

/** Read an operator-owned roster with errors that say how to recover. */
export async function readFleetRosterText(path: string): Promise<string> {
  try {
    return await readFile(path, 'utf8');
  } catch (error) {
    if (isNodeErrorCode(error, 'ENOENT')) {
      throw new FleetRosterConfigurationError(
        `No fleet roster found at ${path}. Run \`mosaic fleet init\` to create one.`,
      );
    }
    throw new FleetRosterConfigurationError(
      `Could not read fleet roster at ${path}. Check the file exists and is readable.`,
    );
  }
}

/** Parse a roster document needed only to select the v1/v2 command path. */
export function parseFleetRosterDocument(source: string, path: string): unknown {
  try {
    return YAML.parse(source);
  } catch (error) {
    if (isRosterParserError(error)) throw invalidFleetRosterError(path);
    throw error;
  }
}

function invalidFleetRosterError(path: string): FleetRosterConfigurationError {
  return new FleetRosterConfigurationError(
    `Fleet roster at ${path} is invalid. Fix the file or run \`mosaic fleet init --force\`.`,
  );
}

function isRosterParserError(error: unknown): boolean {
  return (
    error instanceof SyntaxError ||
    (error instanceof Error && (error.name === 'YAMLParseError' || error.name === 'YAMLWarning'))
  );
}

export function getRosterAgent(roster: FleetRoster, name: string): FleetAgent {
  const agent = roster.agents.find((candidate) => candidate.name === name);
  if (!agent) throw new Error(`Agent "${name}" is not in the fleet roster.`);
  return agent;
}

export function normalizeFleetRosterV1(raw: RawFleetRoster): FleetRoster {
  try {
    return normalizeFleetRosterV1Unchecked(raw);
  } catch (error) {
    if (error instanceof FleetRosterConfigurationError) throw error;
    if (error instanceof Error) throw new FleetRosterConfigurationError(error.message);
    throw error;
  }
}

function normalizeFleetRosterV1Unchecked(raw: RawFleetRoster): FleetRoster {
  assertObject(raw, 'Fleet roster');
  assertKnownKeys(raw, 'Fleet roster', [
    'version',
    'transport',
    'tmux',
    'defaults',
    'runtimes',
    'agents',
    'connector',
  ]);
  if (raw.tmux !== undefined) {
    assertObject(raw.tmux, 'Fleet roster tmux');
    assertKnownKeys(raw.tmux, 'Fleet roster tmux', [
      'socket_name',
      'socketName',
      'holder_session',
      'holderSession',
    ]);
  }
  if (raw.defaults !== undefined) {
    assertObject(raw.defaults, 'Fleet roster defaults');
    assertKnownKeys(raw.defaults, 'Fleet roster defaults', [
      'working_directory',
      'workingDirectory',
    ]);
  }
  if (raw.runtimes !== undefined) {
    assertObject(raw.runtimes, 'Fleet roster runtimes');
    for (const [runtime, config] of Object.entries(raw.runtimes)) {
      assertObject(config, `Fleet roster runtime "${runtime}"`);
      assertKnownKeys(config, `Fleet roster runtime "${runtime}"`, [
        'reset_command',
        'resetCommand',
      ]);
    }
  }
  if (raw.version !== 1) throw new Error('Fleet roster version must be 1.');
  if (raw.transport !== 'tmux') throw new Error('Fleet roster transport must be "tmux".');
  if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
    throw new Error('Fleet roster must define at least one agent.');
  }

  const socketName = targetingString(
    aliasValue(raw.tmux, 'socket_name', 'socketName', 'Fleet roster tmux socket'),
    '',
    'Fleet roster tmux socket_name',
    /^[A-Za-z0-9_.-]+$/,
  );
  const agents = raw.agents.map(normalizeAgent);
  assertUniqueAgentNames(agents);
  for (const agent of agents) {
    if (agent.socket !== undefined && agent.socket !== socketName) {
      throw new Error(
        `Fleet agent "${agent.name}" socket must equal the fleet-wide tmux socket_name; independent per-agent sockets are not supported.`,
      );
    }
  }
  return {
    version: 1,
    transport: 'tmux',
    tmux: {
      socketName,
      holderSession: stringValue(
        aliasValue(raw.tmux, 'holder_session', 'holderSession', 'Fleet roster tmux holder'),
        DEFAULT_HOLDER_SESSION,
        'Fleet roster tmux holder_session',
      ),
    },
    defaults: {
      workingDirectory: stringValue(
        aliasValue(
          raw.defaults,
          'working_directory',
          'workingDirectory',
          'Fleet roster defaults working directory',
        ),
        DEFAULT_WORKING_DIRECTORY,
        'Fleet roster defaults working_directory',
      ),
    },
    runtimes: normalizeRuntimes(raw.runtimes as RawFleetRoster['runtimes']),
    agents,
    connector: normalizeConnector(raw.connector as RawFleetRoster['connector']),
  };
}

function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): FleetAgent {
  assertObject(raw, 'Fleet roster agent');
  assertKnownKeys(raw, 'Fleet roster agent', [
    'name',
    'alias',
    'provider',
    'runtime',
    'class',
    'host',
    'ssh',
    'socket',
    'working_directory',
    'workingDirectory',
    'model_hint',
    'modelHint',
    'reasoning_level',
    'reasoningLevel',
    'tool_policy',
    'toolPolicy',
    'persistent_persona',
    'persistentPersona',
    'reset_between_tasks',
    'resetBetweenTasks',
    'kickstart_template',
    'kickstartTemplate',
  ]);
  const name = stringValue(raw.name, '', 'Fleet roster agent name');
  const runtime = stringValue(
    raw.runtime,
    '',
    `Fleet roster agent "${name || '<unknown>'}" runtime`,
  );
  if (!name || !/^[A-Za-z0-9_.-]+$/.test(name)) {
    throw new Error(`Invalid fleet agent name: ${name || '<empty>'}`);
  }
  if (!runtime) throw new Error(`Fleet agent "${name}" must define a runtime.`);
  return {
    name,
    alias: optionalString(raw.alias, `Fleet roster agent "${name}" alias`),
    provider: optionalString(raw.provider, `Fleet roster agent "${name}" provider`),
    runtime,
    className: canonicalizeRoleClass(
      stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`),
    ).canonicalClass,
    host: optionalTargetingString(
      raw.host,
      `Fleet roster agent "${name}" host`,
      /^[A-Za-z0-9_.:[\]-]+$/,
    ),
    ssh: optionalTargetingString(
      raw.ssh,
      `Fleet roster agent "${name}" ssh`,
      /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9_.:[\]-]+$/,
    ),
    socket: optionalTargetingString(
      raw.socket,
      `Fleet roster agent "${name}" socket`,
      /^[A-Za-z0-9_.-]+$/,
    ),
    workingDirectory: optionalString(
      aliasValue(
        raw,
        'working_directory',
        'workingDirectory',
        `Fleet roster agent "${name}" working directory`,
      ),
      `Fleet roster agent "${name}" working_directory`,
    ),
    modelHint: optionalString(
      aliasValue(raw, 'model_hint', 'modelHint', `Fleet roster agent "${name}" model hint`),
      `Fleet roster agent "${name}" model_hint`,
    ),
    reasoningLevel: optionalString(
      aliasValue(
        raw,
        'reasoning_level',
        'reasoningLevel',
        `Fleet roster agent "${name}" reasoning level`,
      ),
      `Fleet roster agent "${name}" reasoning_level`,
    ),
    toolPolicy: optionalString(
      aliasValue(raw, 'tool_policy', 'toolPolicy', `Fleet roster agent "${name}" tool policy`),
      `Fleet roster agent "${name}" tool_policy`,
    ),
    persistentPersona: optionalBooleanOrString(
      aliasValue(
        raw,
        'persistent_persona',
        'persistentPersona',
        `Fleet roster agent "${name}" persistent persona`,
      ),
      `Fleet roster agent "${name}" persistent_persona`,
    ),
    resetBetweenTasks: optionalBoolean(
      aliasValue(
        raw,
        'reset_between_tasks',
        'resetBetweenTasks',
        `Fleet roster agent "${name}" reset between tasks`,
      ),
      `Fleet roster agent "${name}" reset_between_tasks`,
    ),
    kickstartTemplate: optionalString(
      aliasValue(
        raw,
        'kickstart_template',
        'kickstartTemplate',
        `Fleet roster agent "${name}" kickstart template`,
      ),
      `Fleet roster agent "${name}" kickstart_template`,
    ),
  };
}

function normalizeRuntimes(
  raw: RawFleetRoster['runtimes'] | undefined,
): Record<string, { resetCommand: string }> {
  const result: Record<string, { resetCommand: string }> = { ...DEFAULT_RUNTIME_RESETS };
  for (const [runtime, config] of Object.entries(raw ?? {})) {
    result[runtime] = {
      resetCommand: stringValue(
        aliasValue(
          config,
          'reset_command',
          'resetCommand',
          `Fleet roster runtime "${runtime}" reset command`,
        ),
        '/clear',
        `Fleet roster runtime "${runtime}" reset_command`,
      ),
    };
  }
  return result;
}

function normalizeConnector(raw: RawFleetRoster['connector']): FleetConnector | undefined {
  if (raw === undefined) return undefined;
  assertObject(raw, 'Fleet roster connector');
  assertKnownKeys(raw, 'Fleet roster connector', ['kind', 'matrix', 'discord']);
  const kind = stringValue(raw.kind, '', 'Fleet roster connector kind');
  if (kind === 'tmux') {
    if (raw.matrix !== undefined || raw.discord !== undefined) {
      throw new Error('Fleet roster tmux connector must not define matrix or discord settings.');
    }
    return { kind };
  }
  if (kind === 'discord') {
    if (raw.matrix !== undefined) {
      throw new Error('Fleet roster discord connector must not define matrix settings.');
    }
    assertObject(raw.discord, 'Fleet roster connector discord');
    assertKnownKeys(raw.discord, 'Fleet roster connector discord', ['channel_id']);
    return {
      kind,
      discord: {
        channelId: requiredString(
          raw.discord.channel_id,
          'Fleet roster connector discord channel_id',
        ),
      },
    };
  }
  if (kind === 'matrix') {
    if (raw.discord !== undefined) {
      throw new Error('Fleet roster matrix connector must not define discord settings.');
    }
    assertObject(raw.matrix, 'Fleet roster connector matrix');
    assertKnownKeys(raw.matrix, 'Fleet roster connector matrix', [
      'homeserver_url',
      'user_id',
      'room_id',
    ]);
    return {
      kind,
      matrix: {
        homeserverUrl: requiredString(
          raw.matrix.homeserver_url,
          'Fleet roster connector matrix homeserver_url',
        ),
        userId: requiredString(raw.matrix.user_id, 'Fleet roster connector matrix user_id'),
        roomId: requiredString(raw.matrix.room_id, 'Fleet roster connector matrix room_id'),
      },
    };
  }
  throw new Error('Fleet roster connector kind must be one of: tmux, discord, matrix.');
}

function aliasValue<T extends Record<string, unknown>>(
  source: T | undefined,
  snake: keyof T,
  camel: keyof T,
  label: string,
): unknown {
  const snakeValue = source?.[snake];
  const camelValue = source?.[camel];
  if (snakeValue !== undefined && camelValue !== undefined && snakeValue !== camelValue) {
    throw new Error(`${label} aliases ${String(snake)} and ${String(camel)} conflict.`);
  }
  return snakeValue ?? camelValue;
}

function isNodeErrorCode(error: unknown, code: string): boolean {
  return error instanceof Error && 'code' in error && error.code === code;
}

function requiredString(value: unknown, label: string): string {
  const resolved = stringValue(value, '', label).trim();
  if (!resolved) throw new Error(`${label} is required.`);
  return resolved;
}

function assertObject(value: unknown, label: string): asserts value is Record<string, unknown> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(`${label} must be an object.`);
  }
}

function assertKnownKeys(
  value: Record<string, unknown>,
  label: string,
  allowedKeys: readonly string[],
): void {
  const allowed = new Set(allowedKeys);
  const unknownKeys = Object.keys(value).filter((key) => !allowed.has(key));
  if (unknownKeys.length > 0) {
    throw new Error(`${label} has unknown field(s): ${unknownKeys.join(', ')}.`);
  }
}

function assertUniqueAgentNames(agents: FleetAgent[]): void {
  const seen = new Set<string>();
  for (const agent of agents) {
    if (seen.has(agent.name)) {
      throw new Error(`Fleet roster has duplicate agent name: ${agent.name}.`);
    }
    seen.add(agent.name);
  }
}

function stringValue(value: unknown, fallback = '', label = 'Value'): string {
  if (value === undefined) return fallback;
  if (typeof value !== 'string') throw new Error(`${label} must be a string.`);
  return value;
}

function targetingString(value: unknown, fallback: string, label: string, pattern: RegExp): string {
  const resolved = stringValue(value, fallback, label);
  if (resolved && !pattern.test(resolved)) {
    throw new Error(`${label} contains unsupported targeting characters.`);
  }
  return resolved;
}

function optionalTargetingString(
  value: unknown,
  label: string,
  pattern: RegExp,
): string | undefined {
  const resolved = optionalString(value, label);
  if (resolved !== undefined && (!resolved || !pattern.test(resolved))) {
    throw new Error(`${label} contains unsupported targeting characters.`);
  }
  return resolved;
}

function optionalString(value: unknown, label = 'Value'): string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'string') throw new Error(`${label} must be a string.`);
  return value;
}

function optionalBoolean(value: unknown, label = 'Value'): boolean | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean.`);
  return value;
}

function optionalBooleanOrString(value: unknown, label = 'Value'): boolean | string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'boolean' && typeof value !== 'string') {
    throw new Error(`${label} must be a boolean or string.`);
  }
  return value;
}
