export type HandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed';

export interface CoordinationScope {
  readonly actorId: string;
  readonly tenantId: string;
  readonly correlationId: string;
  /** Trusted gateway/configuration identity; never supplied by a channel client. */
  readonly requesterAgentId: string;
}

export interface InteractionCoordinationIdentity {
  readonly interactionAgentId: string;
  readonly orchestrationAgentId: string;
}

export interface HandoffRequest {
  readonly idempotencyKey: string;
  readonly summary: string;
  readonly context?: string;
  readonly missionId?: string;
}

export interface Handoff {
  readonly handoffId: string;
  readonly targetAgentId: string;
  readonly request: HandoffRequest;
  readonly scope: CoordinationScope;
}

export interface HandoffReceipt {
  readonly handoffId: string;
  readonly targetAgentId: string;
  readonly status: 'queued' | 'accepted';
  readonly correlationId: string;
}

export interface InteractionCoordinationActivity {
  readonly occurredAt: string;
  readonly status: HandoffStatus;
  readonly summary: string;
}

export interface CoordinationObservation {
  readonly handoffId: string;
  readonly targetAgentId: string;
  readonly status: HandoffStatus;
  readonly correlationId: string;
  readonly activity: readonly InteractionCoordinationActivity[];
}

export interface CoordinationResult {
  readonly handoffId: string;
  readonly targetAgentId: string;
  readonly status: 'completed' | 'failed' | 'pending';
  readonly correlationId: string;
  readonly summary?: string;
}

/**
 * Transport-neutral boundary. The interaction plane can request work and read
 * its progress/result, but it cannot issue worker, review, merge, or other
 * general orchestration commands.
 */
export interface InteractionCoordinationPort {
  handoff(handoff: Handoff): Promise<HandoffReceipt>;
  observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
  result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
}

export type InteractionCoordinationAuthorityErrorCode =
  | 'invalid_identity'
  | 'requester_forbidden'
  | 'target_drift'
  | 'correlation_drift';

export class InteractionCoordinationAuthorityError extends Error {
  constructor(
    readonly code: InteractionCoordinationAuthorityErrorCode,
    message: string,
  ) {
    super(message);
    this.name = InteractionCoordinationAuthorityError.name;
  }
}

/**
 * Enforces the interaction-to-orchestration authority boundary before a
 * transport is reached. Identity names remain configuration data.
 */
export class InteractionCoordinationClient {
  private readonly identity: InteractionCoordinationIdentity;

  constructor(
    identity: InteractionCoordinationIdentity,
    private readonly port: InteractionCoordinationPort,
    private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
  ) {
    this.identity = normalizeIdentity(identity);
  }

  async handoff(request: HandoffRequest, scope: CoordinationScope): Promise<HandoffReceipt> {
    this.assertRequester(scope);
    const handoff: Handoff = {
      handoffId: this.handoffIdFactory(),
      targetAgentId: this.identity.orchestrationAgentId,
      request: snapshotRequest(request),
      scope: snapshotScope(scope),
    };
    const receipt = await this.port.handoff(handoff);
    if (
      receipt.handoffId !== handoff.handoffId ||
      receipt.targetAgentId !== handoff.targetAgentId
    ) {
      throw new InteractionCoordinationAuthorityError(
        'target_drift',
        'Interaction coordination transport returned a mismatched handoff target',
      );
    }
    if (receipt.correlationId !== handoff.scope.correlationId) {
      throw new InteractionCoordinationAuthorityError(
        'correlation_drift',
        'Interaction coordination transport returned a mismatched correlation ID',
      );
    }
    return receipt;
  }

  async observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation> {
    this.assertRequester(scope);
    return this.assertObservation(await this.port.observe(handoffId, snapshotScope(scope)), scope);
  }

  async result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult> {
    this.assertRequester(scope);
    return this.assertResult(await this.port.result(handoffId, snapshotScope(scope)), scope);
  }

  private assertRequester(scope: CoordinationScope): void {
    if (scope.requesterAgentId !== this.identity.interactionAgentId) {
      throw new InteractionCoordinationAuthorityError(
        'requester_forbidden',
        'Requester is not the configured interaction agent',
      );
    }
  }

  private assertObservation(
    observation: CoordinationObservation,
    scope: CoordinationScope,
  ): CoordinationObservation {
    if (observation.targetAgentId !== this.identity.orchestrationAgentId) {
      throw new InteractionCoordinationAuthorityError(
        'target_drift',
        'Interaction coordination transport returned an unexpected observation target',
      );
    }
    if (observation.correlationId !== scope.correlationId) {
      throw new InteractionCoordinationAuthorityError(
        'correlation_drift',
        'Interaction coordination transport returned a mismatched observation correlation ID',
      );
    }
    return observation;
  }

  private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult {
    if (result.targetAgentId !== this.identity.orchestrationAgentId) {
      throw new InteractionCoordinationAuthorityError(
        'target_drift',
        'Interaction coordination transport returned an unexpected result target',
      );
    }
    if (result.correlationId !== scope.correlationId) {
      throw new InteractionCoordinationAuthorityError(
        'correlation_drift',
        'Interaction coordination transport returned a mismatched result correlation ID',
      );
    }
    return result;
  }
}

function normalizeIdentity(
  identity: InteractionCoordinationIdentity,
): InteractionCoordinationIdentity {
  const interactionAgentId = identity.interactionAgentId.trim();
  const orchestrationAgentId = identity.orchestrationAgentId.trim();
  if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) {
    throw new InteractionCoordinationAuthorityError(
      'invalid_identity',
      'Interaction and orchestration identities are required',
    );
  }
  if (interactionAgentId === orchestrationAgentId) {
    throw new InteractionCoordinationAuthorityError(
      'invalid_identity',
      'Interaction and orchestration identities must differ',
    );
  }
  return Object.freeze({ interactionAgentId, orchestrationAgentId });
}

function snapshotRequest(request: HandoffRequest): HandoffRequest {
  return Object.freeze({ ...request });
}

function snapshotScope(scope: CoordinationScope): CoordinationScope {
  return Object.freeze({ ...scope });
}
