import type {
  AgentProvider,
  AttemptRepository,
  ProviderObservation,
  ReconcileRequest,
} from "@awp/application";
import type { Attempt } from "@awp/domain";
import {
  unsafeOpaqueId,
  type AgentRunId,
  type AttemptId,
  type ProviderDescriptor,
  type ProviderError,
  type ProviderId,
  type ProviderOperationContext,
  type ProviderReference,
  type ProviderResult,
  type TaskId,
  type WorkspaceId,
} from "@awp/contracts";

export const ACP_PROVIDER_ID = unsafeOpaqueId<ProviderId>("provider:acp");
export const ACP_SOURCE_PROOF = Object.freeze({
  repository: "agentclientprotocol/typescript-sdk",
  npmPackage: "@agentclientprotocol/sdk",
  reviewedAt: "2026-08-22",
  stableProtocol: "v1",
  sourceRevision: "e6463f444093ed7c5f1cc937c3f32afb5853e906",
  stableSdkVersion: "1.4.0",
  stableSdkTagRevision: "e6463f444093ed7c5f1cc937c3f32afb5853e906",
  codexAdapterRepository: "agentclientprotocol/codex-acp",
  codexAdapterPackage: "@agentclientprotocol/codex-acp",
  codexAdapterVersion: "1.6.2",
  codexAdapterRevision: "ba5bcc3d7759250dde9d4d2286a1bec11b363208",
  experimentalV2: false,
  adapterVersion: "awp-i1-acp-v1-2026-08-22",
} as const);

export type AcpNativeSessionStatus =
  | "starting"
  | "running"
  | "waiting"
  | "checkpointing"
  | "completed"
  | "failed"
  | "cancelling"
  | "cancelled";

export interface AcpNativeCapabilities {
  readonly protocolVersion: string;
  readonly agentName: string;
  readonly capabilities: readonly string[];
}

export interface AcpNativeSession {
  readonly id: string;
  readonly attemptId: string;
  readonly agentRunId: string;
  readonly taskId: string;
  readonly workspaceId: string;
  readonly providerId: string;
  readonly accountId: string;
  readonly model: string;
  readonly status: AcpNativeSessionStatus;
  readonly protocolVersion: string;
  readonly agentName: string;
  readonly capabilities: readonly string[];
  readonly failureKind?: "protocol" | "agent" | "permission" | "cancelled" | "transport";
  readonly recoverable?: boolean;
}

export interface AcpNativeClient {
  capabilities(): Promise<AcpNativeCapabilities>;
  findSessionByIdempotencyKey(
    workspaceId: string,
    idempotencyKey: string,
  ): Promise<AcpNativeSession | undefined>;
  startSession(input: {
    readonly attemptId: string;
    readonly agentRunId: string;
    readonly taskId: string;
    readonly workspaceId: string;
    readonly providerId: string;
    readonly accountId: string;
    readonly model: string;
    readonly idempotencyKey: string;
    readonly correlationId: string;
  }): Promise<AcpNativeSession>;
  cancelSession(nativeSessionId: string): Promise<AcpNativeSession>;
  getSession(nativeSessionId: string): Promise<AcpNativeSession | undefined>;
}

export class AcpAdapterError extends Error {
  constructor(
    readonly providerError: ProviderError,
    options?: ErrorOptions,
  ) {
    super(providerError.safeMessage, options);
    this.name = "AcpAdapterError";
  }
}

function observedNow(): string {
  return new Date().toISOString();
}

function fail(category: ProviderError["category"], safeMessage: string, retryable = false): never {
  throw new AcpAdapterError({
    category,
    retryable,
    providerId: ACP_PROVIDER_ID,
    safeMessage,
    observedAt: observedNow(),
  });
}

function normalizeError(error: unknown): AcpAdapterError {
  if (error instanceof AcpAdapterError) return error;
  const status =
    typeof error === "object" && error !== null && "status" in error
      ? Number((error as { readonly status?: unknown }).status)
      : undefined;
  const category =
    status === 401
      ? "auth"
      : status === 403
        ? "permission"
        : status === 404
          ? "missing-resource"
          : status === 409
            ? "conflict-stale"
            : status === 429
              ? "rate-capacity"
              : status !== undefined && status >= 500
                ? "unavailable"
                : "adapter-protocol";
  return new AcpAdapterError(
    {
      category,
      retryable: category === "unavailable" || category === "rate-capacity",
      providerId: ACP_PROVIDER_ID,
      safeMessage: `ACP provider operation failed (${category})`,
      observedAt: observedNow(),
    },
    { cause: error },
  );
}

function reference(session: AcpNativeSession, observedAt: string): ProviderReference {
  return {
    providerId: ACP_PROVIDER_ID,
    resourceType: "agent-session",
    nativeId: session.id,
    nativeRevision: `${session.status}:${session.protocolVersion}`,
    observedAt,
  };
}

function observation(session: AcpNativeSession): ProviderObservation {
  return {
    state: session.status,
    details: Object.freeze({
      attemptId: session.attemptId,
      agentRunId: session.agentRunId,
      taskId: session.taskId,
      workspaceId: session.workspaceId,
      providerId: session.providerId,
      accountId: session.accountId,
      model: session.model,
      protocolVersion: session.protocolVersion,
      agentName: session.agentName,
      capabilities: Object.freeze([...session.capabilities]),
      ...(session.failureKind === undefined ? {} : { failureKind: session.failureKind }),
    }),
  };
}

function result(session: AcpNativeSession): ProviderResult<ProviderObservation> {
  const observedAt = observedNow();
  return {
    value: observation(session),
    references: [reference(session, observedAt)],
    observedAt,
    reconciliationToken: `${session.id}:${session.status}:${session.protocolVersion}`,
  };
}

function resolvedAttemptProvenance(attempt: Attempt): {
  readonly providerId: string;
  readonly accountId: string;
  readonly model: string;
} {
  if (
    attempt.providerId === undefined ||
    attempt.accountId === undefined ||
    attempt.model === undefined
  ) {
    fail(
      "adapter-protocol",
      "Canonical Attempt is missing resolved provider/account/model provenance",
    );
  }
  if (attempt.providerId !== ACP_PROVIDER_ID) {
    fail("unsupported-capability", "Canonical Attempt is assigned to a different Agent provider");
  }
  return {
    providerId: String(attempt.providerId),
    accountId: attempt.accountId,
    model: attempt.model,
  };
}

function assertSessionIdentity(
  session: AcpNativeSession,
  attempt: Attempt,
  agentRunId: AgentRunId,
  taskId: TaskId,
  workspaceId: WorkspaceId,
): void {
  const provenance = resolvedAttemptProvenance(attempt);
  if (
    session.attemptId !== String(attempt.id) ||
    session.agentRunId !== String(agentRunId) ||
    session.taskId !== String(taskId) ||
    session.workspaceId !== String(workspaceId) ||
    session.providerId !== provenance.providerId ||
    session.accountId !== provenance.accountId ||
    session.model !== provenance.model
  ) {
    fail("conflict-stale", "ACP idempotency key is already bound to another Attempt input");
  }
}

export class AcpAgentProvider implements AgentProvider {
  constructor(
    private readonly client: AcpNativeClient,
    private readonly attempts: AttemptRepository,
  ) {}

  async describe(): Promise<ProviderDescriptor> {
    try {
      const native = await this.client.capabilities();
      const nativeCapabilities = new Set(native.capabilities);
      const awpCapabilities = [
        "agent.reconcile",
        ...(nativeCapabilities.has("session.new") ? ["agent.start"] : []),
        ...(nativeCapabilities.has("session.cancel") ? ["agent.cancel"] : []),
      ];
      return {
        providerId: ACP_PROVIDER_ID,
        kind: "agent-protocol",
        adapterVersion: ACP_SOURCE_PROOF.adapterVersion,
        capabilities: [...awpCapabilities, ...native.capabilities.map((value) => `acp.${value}`)],
        authModes: ["trusted-client-connection"],
        resourceTypes: ["agent-session"],
        healthFeatures: ["protocol-version", "session-state", "failure-kind"],
      };
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async startAttempt(
    context: ProviderOperationContext,
    attemptId: AttemptId,
    agentRunId: AgentRunId,
    taskId: TaskId,
    workspaceId: WorkspaceId,
  ): Promise<ProviderResult<ProviderObservation>> {
    try {
      const attempt = await this.attempts.getById(attemptId);
      if (attempt === undefined) fail("missing-resource", "Canonical Attempt does not exist");
      if (attempt.agentRunId !== agentRunId || attempt.workspaceId !== workspaceId) {
        fail("conflict-stale", "AgentProvider inputs do not match the canonical Attempt");
      }
      const provenance = resolvedAttemptProvenance(attempt);
      const existing = await this.client.findSessionByIdempotencyKey(
        String(workspaceId),
        context.idempotencyKey,
      );
      if (existing !== undefined) {
        assertSessionIdentity(existing, attempt, agentRunId, taskId, workspaceId);
        if (!existing.recoverable) {
          const providerResult = result(existing);
          const nativeReference = providerResult.references[0];
          if (!nativeReference)
            fail("adapter-protocol", "ACP session did not produce a provider reference");
          await this.attempts.bindProviderReference(attempt.id, nativeReference);
          return providerResult;
        }
      }

      // Deliberately do not project context.credentialReferenceId or any reusable
      // publication/provider credential into AgentRun/session authority. Only the
      // canonical Attempt's immutable provider/account/model selections cross the
      // adapter boundary. The ACP native client is constructed by the trusted control plane.
      const session = await this.client.startSession({
        attemptId: String(attempt.id),
        agentRunId: String(agentRunId),
        taskId: String(taskId),
        workspaceId: String(workspaceId),
        providerId: provenance.providerId,
        accountId: provenance.accountId,
        model: provenance.model,
        idempotencyKey: context.idempotencyKey,
        correlationId: String(context.correlationId),
      });
      assertSessionIdentity(session, attempt, agentRunId, taskId, workspaceId);
      const providerResult = result(session);
      const nativeReference = providerResult.references[0];
      if (!nativeReference)
        fail("adapter-protocol", "ACP session did not produce a provider reference");
      await this.attempts.bindProviderReference(attempt.id, nativeReference);
      return providerResult;
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async cancelAttempt(
    context: ProviderOperationContext,
    attemptId: AttemptId,
  ): Promise<ProviderResult<ProviderObservation>> {
    void context;
    try {
      const attempt = await this.attempts.getById(attemptId);
      if (!attempt) fail("missing-resource", "Canonical Attempt does not exist");
      const nativeReference = attempt.providerReference;
      if (!nativeReference || nativeReference.providerId !== ACP_PROVIDER_ID) {
        fail("missing-resource", "Canonical Attempt has no ACP provider reference");
      }
      return this.cancelNativeSession(nativeReference.nativeId);
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async cancelNativeSession(nativeSessionId: string): Promise<ProviderResult<ProviderObservation>> {
    try {
      const session = await this.client.getSession(nativeSessionId);
      if (session === undefined) fail("missing-resource", "ACP session does not exist");
      if (
        session.status === "completed" ||
        session.status === "failed" ||
        session.status === "cancelled"
      ) {
        return result(session);
      }
      return result(await this.client.cancelSession(nativeSessionId));
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async reconcile(request: ReconcileRequest): Promise<ProviderResult<ProviderObservation>> {
    try {
      if (request.reference.providerId !== ACP_PROVIDER_ID) {
        fail("adapter-protocol", "Cannot reconcile a reference owned by another provider");
      }
      const session = await this.client.getSession(request.reference.nativeId);
      if (session === undefined) fail("missing-resource", "ACP session no longer exists");
      return result(session);
    } catch (error) {
      throw normalizeError(error);
    }
  }
}
