import YAML from 'yaml';
import {
  authorityForCanonicalClass,
  canonicalizeRoleClass,
  defaultOverrideDir,
  defaultRolesDir,
  extractClassesFromDir,
  resolvePersonaFrom,
  type PersonaDirs,
  type PersonaResolution,
  type RoleAuthority,
} from '../commands/fleet-personas.js';
import { compareCodePoints } from './deterministic-order.js';

export const ROSTER_V2_SUPPORTED_RUNTIMES = ['claude', 'codex', 'opencode', 'pi'] as const;
export const ROSTER_V2_REASONING_LEVELS = ['low', 'medium', 'high'] as const;
export const ROSTER_V2_DESIRED_STATES = ['running', 'stopped'] as const;

export type RosterV2RuntimeName = (typeof ROSTER_V2_SUPPORTED_RUNTIMES)[number];
export type RosterV2ReasoningLevel = (typeof ROSTER_V2_REASONING_LEVELS)[number];
export type RosterV2DesiredState = (typeof ROSTER_V2_DESIRED_STATES)[number];
export type RosterV2InputFormat = 'json' | 'yaml';

export interface FleetRosterV2Tmux {
  readonly socketName: string;
  readonly holderSession: string;
}

export interface FleetRosterV2Defaults {
  readonly workingDirectory: string;
  readonly runtime: RosterV2RuntimeName;
}

export interface FleetRosterV2Runtime {
  readonly resetCommand: string;
}

export interface FleetRosterV2Lifecycle {
  readonly enabled: boolean;
  readonly desiredState: RosterV2DesiredState;
}

export interface FleetRosterV2Launch {
  readonly yolo: boolean;
}

export interface FleetRosterV2Agent {
  readonly name: string;
  readonly alias: string;
  readonly className: string;
  readonly runtime: RosterV2RuntimeName;
  readonly provider: string;
  readonly model: string;
  readonly reasoning: RosterV2ReasoningLevel;
  readonly toolPolicy: string;
  readonly workingDirectory: string;
  readonly persistentPersona: boolean;
  readonly resetBetweenTasks: boolean;
  readonly lifecycle: FleetRosterV2Lifecycle;
  readonly launch: FleetRosterV2Launch;
}

export interface FleetRosterV2 {
  readonly version: 2;
  readonly generation: number;
  readonly transport: 'tmux';
  readonly tmux: FleetRosterV2Tmux;
  readonly defaults: FleetRosterV2Defaults;
  readonly runtimes: Readonly<Record<string, FleetRosterV2Runtime>>;
  readonly agents: readonly FleetRosterV2Agent[];
}

export interface SemanticallyValidatedRosterV2Agent extends FleetRosterV2Agent {
  readonly requestedClass: string;
  readonly canonicalClass: string;
  readonly canonicalToolPolicy: string;
  readonly persona: PersonaResolution;
  readonly authority: RoleAuthority;
}

export interface SemanticallyValidatedRosterV2 extends Omit<FleetRosterV2, 'agents'> {
  readonly agents: readonly SemanticallyValidatedRosterV2Agent[];
}

const PROTECTED_TOOL_POLICY_CLASSES = new Set([
  'merge-gate',
  'validator',
  'orchestrator',
  'team-leader',
  'interaction',
]);

/**
 * Validate filesystem-backed roster semantics after synchronous structural parsing.
 * Directory scans are batched once and every class must resolve to readable content.
 */
export async function validateRosterV2Semantics(
  roster: FleetRosterV2,
  opts: PersonaDirs = {},
): Promise<SemanticallyValidatedRosterV2> {
  const rolesDir = opts.rolesDir ?? defaultRolesDir(opts.mosaicHome);
  const overrideDir = opts.overrideDir ?? defaultOverrideDir(opts.mosaicHome);
  const [base, over] = await Promise.all([
    extractClassesFromDir(rolesDir),
    extractClassesFromDir(overrideDir),
  ]);

  const agents: SemanticallyValidatedRosterV2Agent[] = [];
  for (const agent of roster.agents) {
    const requestedClass = agent.className;
    const { canonicalClass } = canonicalizeRoleClass(requestedClass);
    const canonicalToolPolicy = canonicalizeRoleClass(agent.toolPolicy).canonicalClass;
    const persona = await resolvePersonaFrom(requestedClass, {
      rolesDir,
      overrideDir,
      base,
      over,
    });
    if (!persona || persona.content.trim() === '') {
      throw new RosterV2ValidationError(
        `Roster v2 agent "${agent.name}" class "${requestedClass}" does not resolve to a readable persona.`,
      );
    }
    if (
      (PROTECTED_TOOL_POLICY_CLASSES.has(canonicalClass) ||
        PROTECTED_TOOL_POLICY_CLASSES.has(canonicalToolPolicy)) &&
      canonicalToolPolicy !== canonicalClass
    ) {
      throw new RosterV2ValidationError(
        `Roster v2 agent "${agent.name}" protected class "${canonicalClass}" tool policy must match its canonical class; received "${agent.toolPolicy}".`,
      );
    }
    agents.push(
      Object.freeze({
        ...agent,
        requestedClass,
        canonicalClass,
        canonicalToolPolicy,
        persona,
        authority: authorityForCanonicalClass(canonicalClass),
      }),
    );
  }
  return Object.freeze({ ...roster, agents: Object.freeze(agents) });
}

export class RosterV2ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'RosterV2ValidationError';
  }
}

type JsonSchema = string | number | boolean | null | JsonSchema[] | { [key: string]: JsonSchema };

/**
 * Executable v2 structural contract. The checked-in documentation schema is
 * structurally compared to this value in roster-v2.spec.ts.
 */
export const ROSTER_V2_JSON_SCHEMA: JsonSchema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  $id: 'https://mosaicstack.dev/schemas/fleet/roster-v2.schema.json',
  title: 'Mosaic local tmux fleet roster v2',
  type: 'object',
  additionalProperties: false,
  required: ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'],
  properties: {
    version: { const: 2 },
    generation: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER },
    transport: { const: 'tmux' },
    tmux: {
      type: 'object',
      additionalProperties: false,
      required: ['socket_name', 'holder_session'],
      properties: {
        socket_name: { type: 'string', pattern: '^[A-Za-z0-9_.-]*$' },
        holder_session: { type: 'string', pattern: '^[A-Za-z0-9_.-]+$' },
      },
    },
    defaults: {
      type: 'object',
      additionalProperties: false,
      required: ['working_directory', 'runtime'],
      properties: {
        working_directory: { type: 'string', minLength: 1 },
        runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
      },
    },
    runtimes: {
      type: 'object',
      minProperties: 1,
      propertyNames: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
      additionalProperties: {
        type: 'object',
        additionalProperties: false,
        required: ['reset_command'],
        properties: { reset_command: { type: 'string', minLength: 1 } },
      },
    },
    agents: {
      type: 'array',
      minItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        required: [
          'name',
          'alias',
          'class',
          'runtime',
          'provider',
          'model',
          'reasoning',
          'tool_policy',
          'working_directory',
          'persistent_persona',
          'reset_between_tasks',
          'lifecycle',
          'launch',
        ],
        properties: {
          name: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]*$' },
          alias: { type: 'string', minLength: 1 },
          class: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
          runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
          provider: { type: 'string', minLength: 1 },
          model: { type: 'string', minLength: 1 },
          reasoning: { enum: [...ROSTER_V2_REASONING_LEVELS] },
          tool_policy: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
          working_directory: { type: 'string', minLength: 1 },
          persistent_persona: { type: 'boolean' },
          reset_between_tasks: { type: 'boolean' },
          lifecycle: {
            type: 'object',
            additionalProperties: false,
            required: ['enabled', 'desired_state'],
            properties: {
              enabled: { type: 'boolean' },
              desired_state: { enum: [...ROSTER_V2_DESIRED_STATES] },
            },
          },
          launch: {
            type: 'object',
            additionalProperties: false,
            required: ['yolo'],
            properties: { yolo: { type: 'boolean' } },
          },
        },
      },
    },
  },
};

const ROOT_KEYS = ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'];
const TMUX_KEYS = ['socket_name', 'holder_session'];
const DEFAULT_KEYS = ['working_directory', 'runtime'];
const RUNTIME_KEYS = ['reset_command'];
const AGENT_KEYS = [
  'name',
  'alias',
  'class',
  'runtime',
  'provider',
  'model',
  'reasoning',
  'tool_policy',
  'working_directory',
  'persistent_persona',
  'reset_between_tasks',
  'lifecycle',
  'launch',
];
const LIFECYCLE_KEYS = ['enabled', 'desired_state'];
const LAUNCH_KEYS = ['yolo'];
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const TMUX_SOCKET_IDENTIFIER = /^[A-Za-z0-9_.-]*$/;
const TMUX_IDENTIFIER = /^[A-Za-z0-9_.-]+$/;
const POLICY_IDENTIFIER = /^[a-z][a-z0-9-]*$/;

/** Parses YAML or JSON and compiles only the local-tmux roster v2 contract. */
export function parseRosterV2(source: string, format?: RosterV2InputFormat): FleetRosterV2 {
  const parsed = parseSource(source, format);
  return normalizeRosterV2(parsed);
}

/** Renders canonical snake_case YAML with sorted runtime and agent entries. */
export function renderRosterV2Yaml(roster: FleetRosterV2): string {
  const normalized = normalizeRosterV2(toSourceShape(roster));
  return YAML.stringify(toSourceShape(normalized));
}

function parseSource(source: string, format?: RosterV2InputFormat): unknown {
  const resolvedFormat = format ?? (source.trimStart().startsWith('{') ? 'json' : 'yaml');
  try {
    if (resolvedFormat === 'json') return JSON.parse(source) as unknown;
    return YAML.parse(source) as unknown;
  } catch (error: unknown) {
    const detail = error instanceof Error ? error.message : String(error);
    throw new RosterV2ValidationError(`Roster v2 ${resolvedFormat} parse failed: ${detail}`);
  }
}

export function normalizeRosterV2(raw: unknown): FleetRosterV2 {
  const root = requiredObject(raw, 'Roster v2');
  assertKnownKeys(root, 'Roster v2', ROOT_KEYS);
  if (root.version === 1) {
    throw new RosterV2ValidationError(
      'Roster v2 compiler rejects v1 input; use the existing v1 path until migration.',
    );
  }
  if (root.version !== 2) throw new RosterV2ValidationError('Roster v2 version must be 2.');

  const generation = requiredPositiveInteger(root.generation, 'Roster v2 generation');
  const transport = requiredEnum(root.transport, 'Roster v2 transport', ['tmux'] as const);
  const tmux = normalizeTmux(root.tmux);
  const defaults = normalizeDefaults(root.defaults);
  const runtimes = normalizeRuntimes(root.runtimes);
  const agents = normalizeAgents(root.agents, runtimes);

  if (!runtimes[defaults.runtime]) {
    throw new RosterV2ValidationError(
      `Roster v2 defaults runtime "${defaults.runtime}" must be declared in runtimes.`,
    );
  }
  return { version: 2, generation, transport, tmux, defaults, runtimes, agents };
}

function normalizeTmux(value: unknown): FleetRosterV2Tmux {
  const raw = requiredObject(value, 'Roster v2 tmux');
  assertKnownKeys(raw, 'Roster v2 tmux', TMUX_KEYS);
  return {
    socketName: requiredTmuxSocket(raw.socket_name, 'Roster v2 tmux socket_name'),
    holderSession: requiredTmuxIdentifier(raw.holder_session, 'Roster v2 tmux holder_session'),
  };
}

function normalizeDefaults(value: unknown): FleetRosterV2Defaults {
  const raw = requiredObject(value, 'Roster v2 defaults');
  assertKnownKeys(raw, 'Roster v2 defaults', DEFAULT_KEYS);
  return {
    workingDirectory: requiredString(raw.working_directory, 'Roster v2 defaults working_directory'),
    runtime: requiredRuntime(raw.runtime, 'Roster v2 defaults runtime'),
  };
}

function normalizeRuntimes(value: unknown): Readonly<Record<string, FleetRosterV2Runtime>> {
  const raw = requiredObject(value, 'Roster v2 runtimes');
  const names = Object.keys(raw);
  if (names.length === 0)
    throw new RosterV2ValidationError('Roster v2 runtimes must not be empty.');

  const result: Record<string, FleetRosterV2Runtime> = {};
  for (const name of names.sort(compareCodePoints)) {
    const runtime = requiredRuntime(name, 'Roster v2 runtime name');
    const config = requiredObject(raw[name], `Roster v2 runtime "${runtime}"`);
    assertKnownKeys(config, `Roster v2 runtime "${runtime}"`, RUNTIME_KEYS);
    result[runtime] = {
      resetCommand: requiredString(
        config.reset_command,
        `Roster v2 runtime "${runtime}" reset_command`,
      ),
    };
  }
  return result;
}

function normalizeAgents(
  value: unknown,
  runtimes: Readonly<Record<string, FleetRosterV2Runtime>>,
): readonly FleetRosterV2Agent[] {
  if (!Array.isArray(value) || value.length === 0) {
    throw new RosterV2ValidationError('Roster v2 agents must be a non-empty array.');
  }
  const seen = new Set<string>();
  const agents = value.map((candidate: unknown, index: number): FleetRosterV2Agent => {
    const raw = requiredObject(candidate, `Roster v2 agents[${index}]`);
    assertKnownKeys(raw, `Roster v2 agents[${index}]`, AGENT_KEYS);
    const name = requiredIdentifier(raw.name, `Roster v2 agents[${index}] name`);
    if (seen.has(name))
      throw new RosterV2ValidationError(`Roster v2 has duplicate agent name: ${name}.`);
    seen.add(name);
    const runtime = requiredRuntime(raw.runtime, `Roster v2 agent "${name}" runtime`);
    if (!runtimes[runtime]) {
      throw new RosterV2ValidationError(
        `Roster v2 agent "${name}" runtime "${runtime}" must be declared in runtimes.`,
      );
    }
    return {
      name,
      alias: requiredString(raw.alias, `Roster v2 agent "${name}" alias`),
      className: requiredPolicyIdentifier(raw.class, `Roster v2 agent "${name}" class`),
      runtime,
      provider: requiredString(raw.provider, `Roster v2 agent "${name}" provider`),
      model: requiredString(raw.model, `Roster v2 agent "${name}" model`),
      reasoning: requiredEnum(
        raw.reasoning,
        `Roster v2 agent "${name}" reasoning`,
        ROSTER_V2_REASONING_LEVELS,
      ),
      toolPolicy: requiredPolicyIdentifier(
        raw.tool_policy,
        `Roster v2 agent "${name}" tool_policy`,
      ),
      workingDirectory: requiredString(
        raw.working_directory,
        `Roster v2 agent "${name}" working_directory`,
      ),
      persistentPersona: requiredBoolean(
        raw.persistent_persona,
        `Roster v2 agent "${name}" persistent_persona`,
      ),
      resetBetweenTasks: requiredBoolean(
        raw.reset_between_tasks,
        `Roster v2 agent "${name}" reset_between_tasks`,
      ),
      lifecycle: normalizeLifecycle(raw.lifecycle, name),
      launch: normalizeLaunch(raw.launch, name),
    };
  });
  return agents.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
    compareCodePoints(left.name, right.name),
  );
}

function normalizeLifecycle(value: unknown, agentName: string): FleetRosterV2Lifecycle {
  const raw = requiredObject(value, `Roster v2 agent "${agentName}" lifecycle`);
  assertKnownKeys(raw, `Roster v2 agent "${agentName}" lifecycle`, LIFECYCLE_KEYS);
  return {
    enabled: requiredBoolean(raw.enabled, `Roster v2 agent "${agentName}" lifecycle enabled`),
    desiredState: requiredEnum(
      raw.desired_state,
      `Roster v2 agent "${agentName}" lifecycle desired_state`,
      ROSTER_V2_DESIRED_STATES,
    ),
  };
}

function normalizeLaunch(value: unknown, agentName: string): FleetRosterV2Launch {
  const raw = requiredObject(value, `Roster v2 agent "${agentName}" launch`);
  assertKnownKeys(raw, `Roster v2 agent "${agentName}" launch`, LAUNCH_KEYS);
  return { yolo: requiredBoolean(raw.yolo, `Roster v2 agent "${agentName}" launch yolo`) };
}

function requiredObject(value: unknown, label: string): Record<string, unknown> {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    throw new RosterV2ValidationError(`${label} must be an object.`);
  }
  return value as Record<string, unknown>;
}

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

function requiredString(value: unknown, label: string): string {
  if (typeof value !== 'string' || value.trim() === '') {
    throw new RosterV2ValidationError(`${label} is required and must be a non-empty string.`);
  }
  return value.trim();
}

function requiredIdentifier(value: unknown, label: string): string {
  const result = requiredString(value, label);
  if (!IDENTIFIER.test(result)) {
    throw new RosterV2ValidationError(`Invalid agent name (${label}): ${result}.`);
  }
  return result;
}

function requiredTmuxSocket(value: unknown, label: string): string {
  if (typeof value !== 'string') {
    throw new RosterV2ValidationError(`${label} is required and must be a string.`);
  }
  const result = value.trim();
  if (!TMUX_SOCKET_IDENTIFIER.test(result)) {
    throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
  }
  return result;
}

function requiredTmuxIdentifier(value: unknown, label: string): string {
  const result = requiredString(value, label);
  if (!TMUX_IDENTIFIER.test(result))
    throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
  return result;
}

function requiredPolicyIdentifier(value: unknown, label: string): string {
  const result = requiredString(value, label);
  if (!POLICY_IDENTIFIER.test(result))
    throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
  return result;
}

function requiredPositiveInteger(value: unknown, label: string): number {
  if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
    throw new RosterV2ValidationError(`${label} must be a positive integer.`);
  }
  return value;
}

function requiredBoolean(value: unknown, label: string): boolean {
  if (typeof value !== 'boolean') throw new RosterV2ValidationError(`${label} must be a boolean.`);
  return value;
}

function requiredRuntime(value: unknown, label: string): RosterV2RuntimeName {
  return requiredEnum(value, label, ROSTER_V2_SUPPORTED_RUNTIMES);
}

function requiredEnum<T extends string>(value: unknown, label: string, allowed: readonly T[]): T {
  if (typeof value !== 'string' || !allowed.includes(value as T)) {
    throw new RosterV2ValidationError(
      `${label} must be one of the supported values: ${allowed.join(', ')}.`,
    );
  }
  return value as T;
}

function toSourceShape(roster: FleetRosterV2): Record<string, unknown> {
  return {
    version: roster.version,
    generation: roster.generation,
    transport: roster.transport,
    tmux: { socket_name: roster.tmux.socketName, holder_session: roster.tmux.holderSession },
    defaults: {
      working_directory: roster.defaults.workingDirectory,
      runtime: roster.defaults.runtime,
    },
    runtimes: Object.fromEntries(
      Object.entries(roster.runtimes)
        .sort(([left], [right]): number => compareCodePoints(left, right))
        .map(([name, runtime]): [string, unknown] => [
          name,
          { reset_command: runtime.resetCommand },
        ]),
    ),
    agents: [...roster.agents]
      .sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
        compareCodePoints(left.name, right.name),
      )
      .map(
        (agent: FleetRosterV2Agent): Record<string, unknown> => ({
          name: agent.name,
          alias: agent.alias,
          class: agent.className,
          runtime: agent.runtime,
          provider: agent.provider,
          model: agent.model,
          reasoning: agent.reasoning,
          tool_policy: agent.toolPolicy,
          working_directory: agent.workingDirectory,
          persistent_persona: agent.persistentPersona,
          reset_between_tasks: agent.resetBetweenTasks,
          lifecycle: {
            enabled: agent.lifecycle.enabled,
            desired_state: agent.lifecycle.desiredState,
          },
          launch: { yolo: agent.launch.yolo },
        }),
      ),
  };
}
