import { Inject, Injectable } from '@nestjs/common';
import type {
  HarnessActorContext,
  HarnessAdapter,
  HarnessCatalog,
  HarnessCloseReason,
  HarnessInteractionResponse,
  HarnessSelection,
  HarnessSessionHandle,
  HarnessSessionSnapshot,
} from '@mosaicstack/types';
import {
  HarnessAdapterUnavailableError,
  HarnessRegistry,
  operationError,
} from './harness.registry.js';
import { HARNESS_REGISTRY } from './harness.tokens.js';

/**
 * Trusted, server-derived authority. In production this is produced by the
 * Gateway from the authenticated session — never from a browser/caller DTO.
 */
export interface TrustedGatewayScope {
  readonly actorId: string;
  readonly tenantId: string;
  readonly seatId: string;
  readonly correlationId: string;
}

/** Server-authority fields that must never arrive from an external request DTO. */
const FORBIDDEN_REQUEST_FIELDS = [
  'actorId',
  'tenantId',
  'correlationId',
  'seatId',
  'seat',
  'executable',
  'executablePath',
  'home',
  'homeDir',
  'cwd',
  'workingDir',
  'workingDirectory',
  'nativeSessionPath',
  'sessionPath',
] as const;

/** Raised when an external request DTO smuggles a server-authority field. */
export class HarnessScopeViolationError extends Error {
  constructor(readonly field: string) {
    super(`External request supplied server-authority field "${field}".`);
    this.name = 'HarnessScopeViolationError';
  }
}

export interface CreateHarnessSessionRequest {
  readonly conversationId: string;
  readonly selection: HarnessSelection;
}

export interface ResumeHarnessSessionRequest {
  readonly conversationId: string;
  readonly nativeSessionId: string;
  readonly selection: HarnessSelection;
}

export interface AttachClientRequest {
  readonly conversationId: string;
  readonly clientId: string;
}

export interface DetachClientRequest {
  readonly conversationId: string;
  readonly clientId: string;
}

export interface EvictSessionRequest {
  readonly conversationId: string;
  readonly reason: HarnessCloseReason;
}

export interface EndSessionRequest {
  readonly conversationId: string;
  readonly reason: HarnessCloseReason;
}

export interface RespondInteractionRequest {
  readonly conversationId: string;
  readonly response: HarnessInteractionResponse;
}

interface ActiveSession {
  readonly harnessId: string;
  readonly handle: HarnessSessionHandle;
  readonly correlationId: string;
}

/**
 * Harness-neutral service. It derives the {@link HarnessActorContext} strictly
 * from trusted Gateway scope, validates the selected provider/model tuple with
 * NO fallback substitution, and exposes distinct create/resume/detach/evict/end
 * lifecycle operations.
 */
@Injectable()
export class HarnessService {
  private readonly sessions = new Map<string, ActiveSession>();

  constructor(@Inject(HARNESS_REGISTRY) private readonly registry: HarnessRegistry) {}

  async createSession(
    scope: TrustedGatewayScope,
    request: CreateHarnessSessionRequest,
  ): Promise<HarnessSessionSnapshot> {
    assertTrustedRequest(request);
    const { conversationId, selection } = request;
    const adapter = this.resolveAdapter(scope, selection);
    const context = deriveActorContext(scope);
    await this.assertSelectionAvailable(scope, adapter.catalog(context), selection);

    const handle = await adapter.create({ context, conversationId, selection });
    this.sessions.set(conversationId, {
      harnessId: selection.harnessId,
      handle,
      correlationId: scope.correlationId,
    });
    return handle.snapshot();
  }

  async resumeSession(
    scope: TrustedGatewayScope,
    request: ResumeHarnessSessionRequest,
  ): Promise<HarnessSessionSnapshot> {
    assertTrustedRequest(request);
    const { conversationId, nativeSessionId, selection } = request;
    const adapter = this.resolveAdapter(scope, selection);
    const context = deriveActorContext(scope);
    await this.assertSelectionAvailable(scope, adapter.catalog(context), selection);

    const handle = await adapter.resume({ context, conversationId, nativeSessionId, selection });
    this.sessions.set(conversationId, {
      harnessId: selection.harnessId,
      handle,
      correlationId: scope.correlationId,
    });
    return handle.snapshot();
  }

  async attach(
    scope: TrustedGatewayScope,
    request: AttachClientRequest,
  ): Promise<HarnessSessionSnapshot> {
    assertTrustedRequest(request);
    const handle = this.requireHandle(scope, request.conversationId);
    await handle.attach({ clientId: request.clientId });
    return handle.snapshot();
  }

  async detach(
    scope: TrustedGatewayScope,
    request: DetachClientRequest,
  ): Promise<HarnessSessionSnapshot> {
    assertTrustedRequest(request);
    const handle = this.requireHandle(scope, request.conversationId);
    await handle.detach(request.clientId);
    return handle.snapshot();
  }

  async evict(
    scope: TrustedGatewayScope,
    request: EvictSessionRequest,
  ): Promise<HarnessSessionSnapshot> {
    assertTrustedRequest(request);
    const handle = this.requireHandle(scope, request.conversationId);
    await handle.evictProcess(request.reason);
    return handle.snapshot();
  }

  async end(
    scope: TrustedGatewayScope,
    request: EndSessionRequest,
  ): Promise<HarnessSessionSnapshot> {
    assertTrustedRequest(request);
    const handle = this.requireHandle(scope, request.conversationId);
    await handle.endSession(request.reason);
    const snapshot = await handle.snapshot();
    this.sessions.delete(request.conversationId);
    return snapshot;
  }

  async respondInteraction(
    scope: TrustedGatewayScope,
    request: RespondInteractionRequest,
  ): Promise<void> {
    assertTrustedRequest(request);
    const handle = this.requireHandle(scope, request.conversationId);
    await handle.respondInteraction(request.response);
  }

  async snapshot(
    scope: TrustedGatewayScope,
    conversationId: string,
  ): Promise<HarnessSessionSnapshot> {
    const handle = this.requireHandle(scope, conversationId);
    return handle.snapshot();
  }

  private resolveAdapter(scope: TrustedGatewayScope, selection: HarnessSelection): HarnessAdapter {
    try {
      return this.registry.get(selection.harnessId);
    } catch (error) {
      if (error instanceof HarnessAdapterUnavailableError) {
        throw operationError('adapter_unavailable', error.message, selection, scope.correlationId);
      }
      throw error;
    }
  }

  private async assertSelectionAvailable(
    scope: TrustedGatewayScope,
    catalogPromise: Promise<HarnessCatalog>,
    selection: HarnessSelection,
  ): Promise<void> {
    const catalog = await catalogPromise;
    const entry = catalog.models.find(
      (candidate) =>
        candidate.harnessId === selection.harnessId &&
        candidate.providerId === selection.providerId &&
        candidate.modelId === selection.modelId,
    );
    if (!entry) {
      // No first-row fallback: reject the requested tuple unchanged.
      throw operationError(
        'selection_invalid',
        'The requested harness/provider/model tuple is not in the catalog.',
        selection,
        scope.correlationId,
      );
    }
    if (entry.availability === 'unavailable') {
      throw operationError(
        'model_unavailable',
        'The requested model is currently unavailable.',
        selection,
        scope.correlationId,
        true,
      );
    }
  }

  private requireHandle(scope: TrustedGatewayScope, conversationId: string): HarnessSessionHandle {
    const active = this.sessions.get(conversationId);
    if (!active) {
      throw operationError(
        'session_not_found',
        `No active harness session for conversation "${conversationId}".`,
        { harnessId: '', providerId: '', modelId: '' },
        scope.correlationId,
      );
    }
    return active.handle;
  }
}

/** Build the actor context strictly from trusted scope. No caller data leaks in. */
export function deriveActorContext(scope: TrustedGatewayScope): HarnessActorContext {
  return {
    actorId: scope.actorId,
    tenantId: scope.tenantId,
    seatId: scope.seatId,
    correlationId: scope.correlationId,
  };
}

/** Reject any request object that carries a server-authority field. */
function assertTrustedRequest(request: object): void {
  for (const field of FORBIDDEN_REQUEST_FIELDS) {
    if (Object.prototype.hasOwnProperty.call(request, field)) {
      throw new HarnessScopeViolationError(field);
    }
  }
}

// Re-export the typed operation error so callers importing from the service
// have the discriminated failure type without reaching into the registry.
export { HarnessOperationError } from './harness.registry.js';
