import type {
  AttachClient,
  CreateHarnessSession,
  HarnessAdapter,
  HarnessActorContext,
  HarnessCapability,
  HarnessCatalog,
  HarnessCatalogEntry,
  HarnessCloseReason,
  HarnessDescriptor,
  HarnessEvent,
  HarnessInteractionResponse,
  HarnessPrompt,
  HarnessPromptReceipt,
  HarnessSelection,
  HarnessSessionHandle,
  HarnessSessionSnapshot,
  HarnessSessionState,
  ResumeHarnessSession,
} from '@mosaicstack/types';
import { HARNESS_CAPABILITIES } from '@mosaicstack/types';
import { operationError } from '../harness.registry.js';

export interface FakeHarnessAdapterOptions {
  readonly id: string;
  readonly capabilities?: readonly HarnessCapability[];
  readonly catalog?: readonly HarnessCatalogEntry[];
}

const FAKE_PROVIDER = 'fake-openai';

function defaultCatalog(harnessId: string): readonly HarnessCatalogEntry[] {
  return [
    {
      harnessId,
      providerId: FAKE_PROVIDER,
      modelId: 'fake-mini',
      displayName: 'Fake Mini',
      reasoningCapability: false,
      inputTypes: ['text'],
      authState: 'ready',
      availability: 'available',
    },
    {
      harnessId,
      providerId: FAKE_PROVIDER,
      modelId: 'fake-pro',
      displayName: 'Fake Pro',
      reasoningCapability: true,
      inputTypes: ['text', 'image'],
      authState: 'ready',
      availability: 'available',
    },
    {
      harnessId,
      providerId: FAKE_PROVIDER,
      modelId: 'fake-legacy',
      displayName: 'Fake Legacy',
      reasoningCapability: false,
      inputTypes: ['text'],
      authState: 'unavailable',
      availability: 'unavailable',
    },
  ];
}

function matches(entry: HarnessCatalogEntry, selection: HarnessSelection): boolean {
  return (
    entry.harnessId === selection.harnessId &&
    entry.providerId === selection.providerId &&
    entry.modelId === selection.modelId
  );
}

/**
 * In-memory harness session handle used by the fake adapter and by the shared
 * conformance suite. It enforces the two invariants the real adapters must also
 * honor: model selection is validated against the catalog and is NEVER
 * substituted, and unsupported capabilities fail with a typed error.
 */
export class FakeHarnessSessionHandle implements HarnessSessionHandle {
  private state: HarnessSessionState = 'idle';
  private processId: string | undefined;
  private readonly attachedClientIds = new Set<string>();
  private readonly listeners = new Set<(event: HarnessEvent) => void>();

  constructor(
    private readonly conversationId: string,
    private readonly nativeSessionId: string,
    private readonly seatId: string,
    private selection: HarnessSelection,
    private readonly correlationId: string,
    private readonly capabilities: readonly HarnessCapability[],
    private readonly catalog: readonly HarnessCatalogEntry[],
  ) {
    this.processId = `process-${nativeSessionId}`;
  }

  async snapshot(): Promise<HarnessSessionSnapshot> {
    return {
      conversationId: this.conversationId,
      nativeSessionId: this.nativeSessionId,
      processId: this.processId,
      seatId: this.seatId,
      selection: this.selection,
      state: this.state,
      attachedClientIds: [...this.attachedClientIds],
    };
  }

  async attach(input: AttachClient): Promise<void> {
    this.attachedClientIds.add(input.clientId);
  }

  async detach(clientId: string): Promise<void> {
    // Removes the browser attachment only; the process and native session persist.
    this.attachedClientIds.delete(clientId);
  }

  async prompt(input: HarnessPrompt & { idempotencyKey: string }): Promise<HarnessPromptReceipt> {
    return {
      conversationId: this.conversationId,
      turnId: input.turnId,
      correlationId: input.correlationId,
      state: 'accepted',
      selection: this.selection,
    };
  }

  async setModel(selection: HarnessSelection): Promise<HarnessSelection> {
    const entry = this.catalog.find((candidate) => matches(candidate, selection));
    if (!entry) {
      // No fallback to the first catalog row: reject with the requested tuple, unchanged.
      throw operationError(
        'selection_invalid',
        'The requested harness/provider/model tuple is not in the catalog.',
        selection,
        this.correlationId,
      );
    }
    if (entry.availability === 'unavailable') {
      throw operationError(
        'model_unavailable',
        'The requested model is currently unavailable.',
        selection,
        this.correlationId,
        true,
      );
    }
    this.selection = selection;
    return this.selection;
  }

  async abort(_turnId: string): Promise<void> {
    // No active turn machinery in the fake; abort is a no-op acknowledgement.
  }

  async respondInteraction(_input: HarnessInteractionResponse): Promise<void> {
    if (!this.capabilities.includes('extensionUi')) {
      throw operationError(
        'interaction_unsupported',
        'This harness does not support interactive responses.',
        this.selection,
        this.correlationId,
      );
    }
  }

  events(listener: (event: HarnessEvent) => void): () => void {
    this.listeners.add(listener);
    return () => {
      this.listeners.delete(listener);
    };
  }

  async evictProcess(_reason: HarnessCloseReason): Promise<void> {
    // Stop the process but keep the resumable native session.
    this.processId = undefined;
    this.state = 'evicted';
  }

  async endSession(_reason: HarnessCloseReason): Promise<void> {
    // Destructively end the native session.
    this.processId = undefined;
    this.state = 'ended';
  }
}

/**
 * Minimal in-memory {@link HarnessAdapter} for Slice Zero. It mints a fresh
 * native session id on `create` and binds the supplied one on `resume`, so the
 * two paths are observably distinct.
 */
export class FakeHarnessAdapter implements HarnessAdapter {
  readonly id: string;
  private readonly capabilities: readonly HarnessCapability[];
  private readonly catalogEntries: readonly HarnessCatalogEntry[];
  private createdCount = 0;

  constructor(options: FakeHarnessAdapterOptions) {
    this.id = options.id;
    this.capabilities = options.capabilities ?? [...HARNESS_CAPABILITIES];
    this.catalogEntries = options.catalog ?? defaultCatalog(options.id);
  }

  async describe(_context: HarnessActorContext): Promise<HarnessDescriptor> {
    return {
      id: this.id,
      displayName: `Fake harness (${this.id})`,
      capabilities: this.capabilities,
    };
  }

  async catalog(_context: HarnessActorContext): Promise<HarnessCatalog> {
    return {
      harnessId: this.id,
      version: '1.0.0',
      fingerprint: `fake-${this.id}-${this.catalogEntries.length}`,
      models: this.catalogEntries,
    };
  }

  async create(input: CreateHarnessSession): Promise<HarnessSessionHandle> {
    this.createdCount += 1;
    const nativeSessionId = `native-${input.conversationId}-${this.createdCount}`;
    return new FakeHarnessSessionHandle(
      input.conversationId,
      nativeSessionId,
      input.context.seatId,
      input.selection,
      input.context.correlationId,
      this.capabilities,
      this.catalogEntries,
    );
  }

  async resume(input: ResumeHarnessSession): Promise<HarnessSessionHandle> {
    return new FakeHarnessSessionHandle(
      input.conversationId,
      input.nativeSessionId,
      input.context.seatId,
      input.selection,
      input.context.correlationId,
      this.capabilities,
      this.catalogEntries,
    );
  }
}
