import { Injectable } from '@nestjs/common';
import type {
  HarnessAdapter,
  HarnessErrorCode,
  HarnessErrorDto,
  HarnessSelection,
} from '@mosaicstack/types';

/**
 * A typed harness operation failure that carries a fully-formed, browser-safe
 * {@link HarnessErrorDto}. The DTO's `selection` is always the exact requested
 * tuple — there is no field through which a substituted "effective" selection
 * could ever be reported.
 */
export class HarnessOperationError extends Error {
  readonly code: HarnessErrorCode;
  readonly dto: HarnessErrorDto;

  constructor(dto: HarnessErrorDto) {
    super(dto.message);
    this.name = 'HarnessOperationError';
    this.code = dto.code;
    this.dto = dto;
  }
}

/** Build a {@link HarnessOperationError} that echoes the requested selection unchanged. */
export function operationError(
  code: HarnessErrorCode,
  message: string,
  selection: HarnessSelection,
  correlationId: string,
  retryable = false,
): HarnessOperationError {
  return new HarnessOperationError({ code, message, retryable, correlationId, selection });
}

/** Raised when an unknown harness id is looked up. Discriminated by `code`. */
export class HarnessAdapterUnavailableError extends Error {
  readonly code = 'adapter_unavailable' as const satisfies HarnessErrorCode;

  constructor(readonly harnessId: string) {
    super(`No harness adapter is registered for id "${harnessId}".`);
    this.name = 'HarnessAdapterUnavailableError';
  }
}

export type HarnessRegistrationFailure = 'blank_id' | 'duplicate_id';

/** Raised when an adapter cannot be registered (blank or duplicate id). */
export class HarnessRegistrationError extends Error {
  constructor(
    readonly reason: HarnessRegistrationFailure,
    readonly harnessId: string,
  ) {
    super(
      reason === 'blank_id'
        ? 'A harness adapter id must be a non-empty string.'
        : `A harness adapter is already registered for id "${harnessId}".`,
    );
    this.name = 'HarnessRegistrationError';
  }
}

/**
 * Harness-neutral adapter registry. Adapters are keyed by their harness id.
 * Registration rejects blank and duplicate ids; lookup of an unknown id fails
 * with {@link HarnessAdapterUnavailableError} (`adapter_unavailable`).
 */
@Injectable()
export class HarnessRegistry {
  private readonly adapters = new Map<string, HarnessAdapter>();

  register(adapter: HarnessAdapter): void {
    const id = adapter.id;
    if (typeof id !== 'string' || id.trim().length === 0) {
      throw new HarnessRegistrationError('blank_id', id ?? '');
    }
    if (this.adapters.has(id)) {
      throw new HarnessRegistrationError('duplicate_id', id);
    }
    this.adapters.set(id, adapter);
  }

  get(harnessId: string): HarnessAdapter {
    const adapter = this.adapters.get(harnessId);
    if (!adapter) {
      throw new HarnessAdapterUnavailableError(harnessId);
    }
    return adapter;
  }

  has(harnessId: string): boolean {
    return this.adapters.has(harnessId);
  }

  list(): readonly HarnessAdapter[] {
    return [...this.adapters.values()];
  }
}
