import { ciscoIosDefaultArpCommand, ciscoIosDefaultTerminalLengthCommand } from './cisco_ios.constants.js';
import { CiscoIosMapper } from './cisco_ios.mapper.js';
import type { ICiscoIosCommand, ICiscoIosCommandResult, ICiscoIosConfig, ICiscoIosEvent, ICiscoIosNativeClient, ICiscoIosSnapshot, ICiscoIosValueMap, TCiscoIosSnapshotSource } from './cisco_ios.types.js';

type TCiscoIosEventHandler = (eventArg: ICiscoIosEvent) => void;
type TCiscoIosRawCommandResponse = ICiscoIosCommandResult | string | ICiscoIosValueMap | boolean | void;
type TCiscoIosExecutor = (commandArg: ICiscoIosCommand) => Promise<TCiscoIosRawCommandResponse>;

export class CiscoIosClient {
  private snapshot?: ICiscoIosSnapshot;
  private readonly events: ICiscoIosEvent[] = [];
  private readonly eventHandlers = new Set<TCiscoIosEventHandler>();

  constructor(private readonly config: ICiscoIosConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<ICiscoIosSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return this.cloneSnapshot(this.snapshot);
    }

    if (!forceRefreshArg) {
      const staticSnapshot = this.staticSnapshot();
      if (staticSnapshot) {
        this.snapshot = staticSnapshot;
        return this.cloneSnapshot(this.snapshot);
      }
    }

    try {
      const liveSnapshot = await this.liveSnapshot();
      if (liveSnapshot) {
        this.snapshot = liveSnapshot;
        return this.cloneSnapshot(this.snapshot);
      }

      const staticSnapshot = this.staticSnapshot();
      if (staticSnapshot) {
        this.snapshot = staticSnapshot;
        return this.cloneSnapshot(this.snapshot);
      }

      this.snapshot = this.offlineSnapshot(this.unsupportedReadMessage());
      return this.cloneSnapshot(this.snapshot);
    } catch (errorArg) {
      this.snapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      return this.cloneSnapshot(this.snapshot);
    }
  }

  public onEvent(handlerArg: TCiscoIosEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async refresh(): Promise<ICiscoIosCommandResult> {
    this.snapshot = undefined;
    const livePath = this.hasLiveReadPath();
    const snapshot = await this.getSnapshot(livePath);
    this.emit({ type: 'snapshot_refreshed', data: this.cloneSnapshot(snapshot), timestamp: Date.now() });
    const success = snapshot.connected && !snapshot.error;
    return {
      success,
      transmitted: livePath && (snapshot.source === 'client' || snapshot.source === 'executor' || snapshot.source === 'transport'),
      action: 'refresh',
      data: { snapshot: this.cloneSnapshot(snapshot), source: snapshot.source },
      error: success ? undefined : snapshot.error,
    };
  }

  public async sendCommand(commandArg: ICiscoIosCommand): Promise<ICiscoIosCommandResult> {
    this.emit({ type: 'command_mapped', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, timestamp: Date.now() });
    try {
      const result = await this.executeCommand(commandArg);
      if (result.success) {
        this.patchSnapshot(commandArg, result);
      }
      this.emit({
        type: result.success ? 'command_succeeded' : 'command_failed',
        command: commandArg,
        deviceId: commandArg.deviceId,
        entityId: commandArg.entityId,
        data: result,
        error: result.error,
        timestamp: Date.now(),
      });
      return result;
    } catch (errorArg) {
      const result: ICiscoIosCommandResult = { success: false, transmitted: false, action: commandArg.action, error: this.errorMessage(errorArg), data: { command: commandArg } };
      this.emit({ type: 'command_failed', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, data: result, error: result.error, timestamp: Date.now() });
      return result;
    }
  }

  public async destroy(): Promise<void> {
    this.eventHandlers.clear();
    await this.config.nativeClient?.destroy?.();
  }

  private staticSnapshot(): ICiscoIosSnapshot | undefined {
    if (this.config.snapshot) {
      return CiscoIosMapper.toSnapshot(this.config, { source: 'snapshot', connected: this.config.snapshot.connected }, this.events);
    }
    const rawArpOutput = this.config.arpOutput || this.config.rawArpOutput;
    if (rawArpOutput) {
      return CiscoIosMapper.toSnapshot(this.config, { source: 'manual', connected: this.config.connected ?? true, rawArpOutput }, this.events);
    }
    return undefined;
  }

  private async liveSnapshot(): Promise<ICiscoIosSnapshot | undefined> {
    if (this.config.transport?.readSnapshot) {
      const result = await this.config.transport.readSnapshot();
      return this.normalizeSnapshotResult(result, 'transport');
    }
    if (this.config.nativeClient) {
      return this.snapshotFromNativeClient(this.config.nativeClient);
    }
    const executor = this.executor();
    if (executor) {
      const output = await this.collectArpOutput(executor, 'executor');
      return CiscoIosMapper.toSnapshot({ ...this.config, arpOutput: output }, { source: 'executor', connected: true, rawArpOutput: output, device: { authenticated: true } }, this.events);
    }
    if (this.config.transport?.execute) {
      const output = await this.collectArpOutput(this.config.transport.execute.bind(this.config.transport), 'transport');
      return CiscoIosMapper.toSnapshot({ ...this.config, arpOutput: output }, { source: 'transport', connected: true, rawArpOutput: output, device: { authenticated: true } }, this.events);
    }
    return undefined;
  }

  private async snapshotFromNativeClient(clientArg: ICiscoIosNativeClient): Promise<ICiscoIosSnapshot> {
    await clientArg.login?.();
    if (clientArg.getSnapshot) {
      return this.normalizeSnapshotResult(await clientArg.getSnapshot(), 'client');
    }
    const readArpOutput = clientArg.getArpOutput || clientArg.readArpOutput;
    if (readArpOutput) {
      const output = await readArpOutput.call(clientArg);
      return CiscoIosMapper.toSnapshot({ ...this.config, arpOutput: output }, { source: 'client', connected: true, rawArpOutput: output, device: { authenticated: true } }, this.events);
    }
    const executor = clientArg.executeCommand || clientArg.execute;
    if (executor) {
      const output = await this.collectArpOutput(executor.bind(clientArg), 'client');
      return CiscoIosMapper.toSnapshot({ ...this.config, arpOutput: output }, { source: 'client', connected: true, rawArpOutput: output, device: { authenticated: true } }, this.events);
    }
    throw new Error('Cisco IOS nativeClient must expose getSnapshot(), getArpOutput(), readArpOutput(), executeCommand(), or execute().');
  }

  private normalizeSnapshotResult(resultArg: ICiscoIosSnapshot | Partial<ICiscoIosSnapshot>, sourceArg: TCiscoIosSnapshotSource): ICiscoIosSnapshot {
    if (this.isSnapshot(resultArg)) {
      return CiscoIosMapper.mergeSnapshot(CiscoIosMapper.toSnapshot(this.config, { source: sourceArg }, this.events), resultArg);
    }
    return CiscoIosMapper.mergeSnapshot(CiscoIosMapper.toSnapshot(this.config, { source: sourceArg, connected: true }, this.events), resultArg);
  }

  private async executeCommand(commandArg: ICiscoIosCommand): Promise<ICiscoIosCommandResult> {
    if (commandArg.action === 'refresh') {
      return this.refresh();
    }
    const executor = this.executor();
    if (executor) {
      return this.commandResult(await this.executeCliCommand(executor, commandArg), { ...commandArg, metadata: { ...(commandArg.metadata || {}), source: 'executor' } }, true);
    }
    if (this.config.transport?.execute) {
      return this.commandResult(await this.executeCliCommand(this.config.transport.execute.bind(this.config.transport), commandArg), { ...commandArg, metadata: { ...(commandArg.metadata || {}), source: 'transport' } }, true);
    }
    if (this.config.nativeClient) {
      await this.config.nativeClient.login?.();
      const nativeExecutor = this.config.nativeClient.executeCommand || this.config.nativeClient.execute;
      if (nativeExecutor) {
        return this.commandResult(await this.executeCliCommand(nativeExecutor.bind(this.config.nativeClient), commandArg), { ...commandArg, metadata: { ...(commandArg.metadata || {}), source: 'client' } }, true);
      }
      if (commandArg.action === 'show_ip_arp') {
        const readArpOutput = this.config.nativeClient.getArpOutput || this.config.nativeClient.readArpOutput;
        if (readArpOutput) {
          return this.commandResult(await readArpOutput.call(this.config.nativeClient), { ...commandArg, metadata: { ...(commandArg.metadata || {}), source: 'client' } }, true);
        }
      }
    }
    return {
      success: false,
      transmitted: false,
      action: commandArg.action,
      error: 'Cisco IOS CLI commands require an injected nativeClient, commandExecutor, or transport.execute. Static snapshots and manual ARP output are read-only.',
      data: { command: commandArg },
    };
  }

  private async collectArpOutput(executorArg: TCiscoIosExecutor, sourceArg: TCiscoIosSnapshotSource): Promise<string> {
    const result = await this.executeCliCommand(executorArg, this.showIpArpCommand(sourceArg));
    const output = this.extractOutput(result);
    if (!output) {
      throw new Error('Cisco IOS ARP command did not return CLI output.');
    }
    return output;
  }

  private async executeCliCommand(executorArg: TCiscoIosExecutor, commandArg: ICiscoIosCommand): Promise<TCiscoIosRawCommandResponse> {
    if (commandArg.action === 'show_ip_arp') {
      const terminalLengthCommand = this.terminalLengthCommand();
      if (terminalLengthCommand) {
        await executorArg({
          action: 'run_command',
          command: terminalLengthCommand,
          service: 'terminal_length',
          target: {},
          host: this.config.host,
          metadata: { setupCommand: true },
        });
      }
    }
    return executorArg(commandArg);
  }

  private commandResult(resultArg: TCiscoIosRawCommandResponse, commandArg: ICiscoIosCommand, transmittedDefaultArg: boolean): ICiscoIosCommandResult {
    if (this.isCommandResult(resultArg)) {
      return { transmitted: transmittedDefaultArg, action: commandArg.action, ...resultArg };
    }
    if (resultArg === false) {
      return { success: false, transmitted: transmittedDefaultArg, action: commandArg.action, error: 'Cisco IOS command executor reported failure.', data: { command: commandArg, response: resultArg } };
    }
    const output = this.extractOutput(resultArg);
    if (resultArg && typeof resultArg === 'object') {
      const record = resultArg as Record<string, unknown>;
      if (record.ok === false || record.error) {
        return { success: false, transmitted: transmittedDefaultArg, action: commandArg.action, error: String(record.error || 'Cisco IOS command returned ok=false.'), output, data: { command: commandArg, response: resultArg } };
      }
    }
    return { success: true, transmitted: transmittedDefaultArg, action: commandArg.action, output, data: { command: commandArg, response: resultArg } };
  }

  private patchSnapshot(commandArg: ICiscoIosCommand, resultArg: ICiscoIosCommandResult): void {
    const source = this.snapshotSource(this.resultCommand(resultArg) || commandArg) || 'executor';
    if (commandArg.action === 'show_ip_arp' && resultArg.output) {
      this.snapshot = CiscoIosMapper.toSnapshot({ ...this.config, arpOutput: resultArg.output }, {
        source,
        connected: true,
        rawArpOutput: resultArg.output,
        metadata: { lastCommand: commandArg.command, lastCommandAt: new Date().toISOString(), lastCommandTransmitted: resultArg.transmitted },
      }, this.events);
    } else {
      const current = this.snapshot || CiscoIosMapper.toSnapshot(this.config, {}, this.events);
      this.snapshot = CiscoIosMapper.mergeSnapshot(current, {
        updatedAt: new Date().toISOString(),
        metadata: { lastCommand: commandArg.command, lastCommandAt: new Date().toISOString(), lastCommandTransmitted: resultArg.transmitted },
      });
    }
    this.emit({ type: 'state_changed', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, data: this.cloneSnapshot(this.snapshot), timestamp: Date.now() });
  }

  private executor(): TCiscoIosExecutor | undefined {
    if (!this.config.commandExecutor) {
      return undefined;
    }
    if (typeof this.config.commandExecutor === 'function') {
      return this.config.commandExecutor;
    }
    return this.config.commandExecutor.execute.bind(this.config.commandExecutor);
  }

  private hasLiveReadPath(): boolean {
    return Boolean(this.config.transport?.readSnapshot || this.config.transport?.execute || this.config.nativeClient || this.config.commandExecutor);
  }

  private showIpArpCommand(sourceArg: TCiscoIosSnapshotSource): ICiscoIosCommand {
    return {
      action: 'show_ip_arp',
      command: this.config.arpCommand || this.snapshot?.commands.arp || ciscoIosDefaultArpCommand,
      service: 'show_ip_arp',
      target: {},
      host: this.config.host,
      transmitted: false,
      metadata: { source: sourceArg },
    };
  }

  private terminalLengthCommand(): string | undefined {
    if (this.config.terminalLengthCommand === false) {
      return undefined;
    }
    return this.config.terminalLengthCommand || this.snapshot?.commands.terminalLength || ciscoIosDefaultTerminalLengthCommand;
  }

  private offlineSnapshot(errorArg: string): ICiscoIosSnapshot {
    return CiscoIosMapper.toSnapshot(this.config, { connected: false, source: 'runtime', error: errorArg, device: { available: false } }, this.events);
  }

  private unsupportedReadMessage(): string {
    return 'No Cisco IOS snapshot, manual ARP output, nativeClient, commandExecutor, or transport.readSnapshot/execute is configured. Native SSH is represented but not faked by this port.';
  }

  private isSnapshot(valueArg: unknown): valueArg is ICiscoIosSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'arpEntries' in valueArg && 'trackedDevices' in valueArg);
  }

  private isCommandResult(valueArg: unknown): valueArg is ICiscoIosCommandResult {
    return Boolean(valueArg && typeof valueArg === 'object' && 'success' in valueArg);
  }

  private extractOutput(valueArg: TCiscoIosRawCommandResponse): string | undefined {
    if (typeof valueArg === 'string') {
      return valueArg;
    }
    if (valueArg && typeof valueArg === 'object') {
      const record = valueArg as Record<string, unknown>;
      for (const key of ['output', 'stdout', 'result', 'data']) {
        const value = record[key];
        if (typeof value === 'string') {
          return value;
        }
      }
      const data = record.data;
      if (data && typeof data === 'object') {
        const nested = data as Record<string, unknown>;
        if (typeof nested.output === 'string') {
          return nested.output;
        }
      }
    }
    return undefined;
  }

  private snapshotSource(commandArg: ICiscoIosCommand): TCiscoIosSnapshotSource | undefined {
    const source = commandArg.metadata?.source;
    return source === 'client' || source === 'transport' || source === 'executor' ? source : undefined;
  }

  private resultCommand(resultArg: ICiscoIosCommandResult): ICiscoIosCommand | undefined {
    const data = resultArg.data;
    if (data && typeof data === 'object') {
      const command = (data as Record<string, unknown>).command;
      if (command && typeof command === 'object' && 'action' in command && 'command' in command) {
        return command as ICiscoIosCommand;
      }
    }
    return undefined;
  }

  private emit(eventArg: ICiscoIosEvent): void {
    this.events.push(eventArg);
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

  private cloneSnapshot(snapshotArg: ICiscoIosSnapshot): ICiscoIosSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as ICiscoIosSnapshot;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}
