import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { HarnessSelection } from '@mosaicstack/types';
import type { ActorTenantScope } from '../auth/session-scope.js';
import {
  HarnessAdapterUnavailableError,
  HarnessRegistry,
  operationError,
} from './harness.registry.js';
import { HARNESS_REGISTRY } from './harness.tokens.js';
import { readContextFromScope } from './harness.dto.js';
import { HarnessSelectionRepository } from './harness-selection.repository.js';

/**
 * Selection logic for the Slice-Zero chat-preferences surface. It validates the
 * requested harness/provider/model tuple against the live catalog with NO
 * fallback substitution, then persists it owner-scoped. The stored selection is
 * only ever mutated when the tuple is valid AND available.
 */
@Injectable()
export class HarnessSelectionService {
  constructor(
    @Inject(HARNESS_REGISTRY) private readonly registry: HarnessRegistry,
    private readonly repository: HarnessSelectionRepository,
  ) {}

  getSelection(scope: ActorTenantScope): HarnessSelection | null {
    return this.repository.get(scope);
  }

  async setSelection(
    scope: ActorTenantScope,
    selection: HarnessSelection,
  ): Promise<HarnessSelection> {
    // Throws HarnessOperationError (selection_invalid / model_unavailable) with the
    // requested tuple echoed back unchanged. The store is untouched on any throw.
    await this.assertSelectionAvailable(scope, selection);
    return this.repository.set(scope, selection);
  }

  private async assertSelectionAvailable(
    scope: ActorTenantScope,
    selection: HarnessSelection,
  ): Promise<void> {
    const correlationId = randomUUID();

    let adapter;
    try {
      adapter = this.registry.get(selection.harnessId);
    } catch (error) {
      if (error instanceof HarnessAdapterUnavailableError) {
        // An unknown harness makes the whole tuple invalid — no fallback adapter.
        throw operationError(
          'selection_invalid',
          'The requested harness/provider/model tuple is not in the catalog.',
          selection,
          correlationId,
        );
      }
      throw error;
    }

    const catalog = await adapter.catalog(readContextFromScope(scope));
    const entry = catalog.models.find(
      (candidate) =>
        candidate.harnessId === selection.harnessId &&
        candidate.providerId === selection.providerId &&
        candidate.modelId === selection.modelId,
    );

    if (!entry) {
      // No first-row / first-provider fallback: reject the requested tuple unchanged.
      throw operationError(
        'selection_invalid',
        'The requested harness/provider/model tuple is not in the catalog.',
        selection,
        correlationId,
      );
    }
    if (entry.availability === 'unavailable') {
      throw operationError(
        'model_unavailable',
        'The requested model is currently unavailable.',
        selection,
        correlationId,
        true,
      );
    }
  }
}
