import { createHash } from "node:crypto";
import type {
  ForgeProvider,
  ProviderObservation,
  ReconcileRequest,
  RepositoryInspection,
  TrustedMerger,
  TrustedMergeReconciliation,
  TrustedMergeRequest,
  TrustedMergeResult,
  TrustedPublisher,
  TrustedPublishRequest,
} from "@awp/application";
import { assertCandidateIdentity, candidateManifestEquals } from "@awp/domain";
import {
  ProviderFailure,
  unsafeOpaqueId,
  type ProviderDescriptor,
  type ProviderError,
  type ProviderId,
  type ProviderOperationContext,
  type ProviderReference,
  type ProviderResult,
} from "@awp/contracts";

export const GITHUB_PROVIDER_ID = unsafeOpaqueId<ProviderId>("provider:github");
export const GITHUB_ADAPTER_VERSION = "awp-i0-github-2026-08-20";

import type {
  GitHubMergeState,
  GitHubPublicationState,
  GitHubRepositoryState,
  GitHubTrustedClient,
} from "./client-types.js";

export class GitHubAdapterError extends ProviderFailure {
  constructor(providerError: ProviderError, options?: ErrorOptions) {
    super(providerError);
    if (options?.cause !== undefined)
      Object.defineProperty(this, "cause", { value: options.cause });
    this.name = "GitHubAdapterError";
  }
}

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

function fail(
  category: ProviderError["category"],
  safeMessage: string,
  retryable = false,
  resourceRef?: ProviderReference,
): never {
  throw new GitHubAdapterError({
    category,
    retryable,
    providerId: GITHUB_PROVIDER_ID,
    ...(resourceRef === undefined ? {} : { resourceRef }),
    safeMessage,
    observedAt: observedNow(),
  });
}

function normalizeError(error: unknown): GitHubAdapterError {
  if (error instanceof GitHubAdapterError) 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 || status === 422
            ? "conflict-stale"
            : status === 429
              ? "rate-capacity"
              : status !== undefined && status >= 500
                ? "unavailable"
                : "adapter-protocol";
  return new GitHubAdapterError(
    {
      category,
      retryable: category === "rate-capacity" || category === "unavailable",
      providerId: GITHUB_PROVIDER_ID,
      safeMessage: `GitHub provider operation failed (${category})`,
      observedAt: observedNow(),
    },
    { cause: error },
  );
}

function repositoryReference(state: GitHubRepositoryState, observedAt: string): ProviderReference {
  return {
    providerId: GITHUB_PROVIDER_ID,
    resourceType: "repository",
    nativeId: state.nativeId,
    nativeRevision: state.headRevision,
    ...(state.url === undefined ? {} : { url: state.url }),
    observedAt,
  };
}

function publicationReference(
  state: GitHubPublicationState,
  observedAt: string,
): ProviderReference {
  return {
    providerId: GITHUB_PROVIDER_ID,
    resourceType: "publication",
    nativeId: state.nativeId,
    nativeRevision: state.targetRevision,
    ...(state.url === undefined ? {} : { url: state.url }),
    observedAt,
  };
}

function mergeReference(state: GitHubMergeState, observedAt: string): ProviderReference {
  return {
    providerId: GITHUB_PROVIDER_ID,
    resourceType: "merge",
    nativeId: state.nativeId,
    nativeRevision: state.resultingRevision,
    ...(state.url === undefined ? {} : { url: state.url }),
    observedAt,
  };
}

function publicationObservation(state: GitHubPublicationState): ProviderObservation {
  return {
    state: state.status,
    details: Object.freeze({
      repositoryKey: state.repositoryKey,
      targetRef: state.targetRef,
      targetRevision: state.targetRevision,
      baseRevision: state.baseRevision,
      candidateDigest: state.candidateDigest,
      candidateManifest: state.candidateManifest,
      factoryRunId: state.factoryRunId,
      idempotencyKey: state.idempotencyKey,
    }),
  };
}

function targetRefFor(changeSetId: TrustedPublishRequest["changeSetId"]): string {
  const safe = String(changeSetId).replace(/[^A-Za-z0-9._-]/g, "-");
  return `refs/heads/awp/${safe}`;
}

function assertTrustedAuthority(context: ProviderOperationContext): void {
  if (context.authority.principal.kind === "agent") {
    fail("permission", "Agent principals cannot invoke the trusted GitHub publication boundary");
  }
}

function assertMergeFidelity(state: GitHubMergeState, request: TrustedMergeRequest): void {
  assertCandidateIdentity(request.candidateDigest, request.candidateManifest);
  if (
    state.repositoryKey !== request.repositoryKey ||
    state.targetRef !== request.expectedTarget.reference ||
    state.expectedTargetRevision !== request.expectedTarget.revision ||
    state.expectedBaseRevision !== request.expectedBase.revision ||
    state.publicationNativeId !== request.publicationReference.nativeId ||
    state.candidateDigest !== request.candidateDigest ||
    !candidateManifestEquals(state.candidateManifest, request.candidateManifest)
  ) {
    fail("conflict-stale", "GitHub merge does not match the requested ChangeSet fidelity");
  }
  if (state.status !== "merged") {
    fail("terminal-provider", `GitHub merge is not usable: ${state.status}`);
  }
}

function assertPublicationFidelity(
  state: GitHubPublicationState,
  request: TrustedPublishRequest,
  targetRef: string,
): void {
  assertCandidateIdentity(request.candidateDigest, request.candidateManifest);
  if (
    state.repositoryKey !== request.repositoryKey ||
    state.baseRevision !== request.baseRevision ||
    state.candidateDigest !== request.candidateDigest ||
    !candidateManifestEquals(state.candidateManifest, request.candidateManifest) ||
    state.factoryRunId !== String(request.factoryRunId) ||
    state.idempotencyKey !== request.context.idempotencyKey ||
    state.targetRef !== targetRef
  ) {
    fail("conflict-stale", "GitHub publication does not match the requested ChangeSet fidelity");
  }
  if (state.status !== "published") {
    fail("terminal-provider", `GitHub publication is not usable: ${state.status}`);
  }
}

export class GitHubForgeProvider implements ForgeProvider {
  constructor(protected readonly client: GitHubTrustedClient) {}

  async describe(): Promise<ProviderDescriptor> {
    return {
      providerId: GITHUB_PROVIDER_ID,
      kind: "forge-vcs",
      adapterVersion: GITHUB_ADAPTER_VERSION,
      capabilities: ["repository.inspect", "publication.reconcile"],
      authModes: ["github-app", "credential-reference"],
      resourceTypes: ["repository", "publication", "ref"],
      healthFeatures: ["rate-limit", "permission", "ref-drift"],
    };
  }

  async inspectRepository(
    context: ProviderOperationContext,
    repositoryKey: string,
  ): Promise<ProviderResult<RepositoryInspection>> {
    void context;
    try {
      const state = await this.client.inspectRepository(
        repositoryKey,
        context.credentialReferenceId,
      );
      const observedAt = observedNow();
      return {
        value: { defaultBranch: state.defaultBranch, headRevision: state.headRevision },
        references: [repositoryReference(state, observedAt)],
        observedAt,
      };
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async reconcile(request: ReconcileRequest): Promise<ProviderResult<ProviderObservation>> {
    try {
      if (request.reference.providerId !== GITHUB_PROVIDER_ID) {
        fail("adapter-protocol", "Cannot reconcile a reference owned by another provider");
      }
      const state = await this.client.readPublication(
        request.reference.nativeId,
        request.context.credentialReferenceId,
      );
      if (state === undefined) fail("missing-resource", "GitHub publication no longer exists");
      const observedAt = observedNow();
      return {
        value: publicationObservation(state),
        references: [publicationReference(state, observedAt)],
        observedAt,
        reconciliationToken: `${state.nativeId}:${state.targetRevision}`,
      };
    } catch (error) {
      throw normalizeError(error);
    }
  }
}

export class GitHubTrustedPublisher implements TrustedPublisher {
  constructor(private readonly client: GitHubTrustedClient) {}

  async publish(request: TrustedPublishRequest): Promise<ProviderResult<ProviderObservation>> {
    try {
      assertTrustedAuthority(request.context);
      const patchDigest = createHash("sha256").update(request.diff).digest("hex");
      if (patchDigest !== request.candidateManifest.patchDigest) {
        fail(
          "conflict-stale",
          "GitHub publication patch digest does not match the ChangeSet manifest",
        );
      }
      const targetRef = targetRefFor(request.changeSetId);
      const existing = await this.client.findPublicationByIdempotencyKey({
        repositoryKey: request.repositoryKey,
        targetRef,
        idempotencyKey: request.context.idempotencyKey,
        credentialReferenceId: request.context.credentialReferenceId,
      });

      let published: GitHubPublicationState;
      if (existing !== undefined) {
        // A replay of an already-completed logical publication must reconcile the
        // exact provider artifact before consulting mutable repository head state.
        // Otherwise a later main-branch advance would make a harmless retry look stale.
        assertPublicationFidelity(existing, request, targetRef);
        published = existing;
      } else {
        const repository = await this.client.inspectRepository(
          request.repositoryKey,
          request.context.credentialReferenceId,
        );
        if (repository.headRevision !== request.baseRevision) {
          fail(
            "conflict-stale",
            "Repository head moved since the ChangeSet base revision was recorded",
            false,
            repositoryReference(repository, observedNow()),
          );
        }
        published = await this.client.publishCandidate({
          repositoryKey: request.repositoryKey,
          targetRef,
          expectedBaseRevision: request.baseRevision,
          candidateDigest: request.candidateDigest,
          candidateManifest: request.candidateManifest,
          diff: request.diff,
          changeSetId: request.changeSetId,
          factoryRunId: request.factoryRunId,
          idempotencyKey: request.context.idempotencyKey,
          credentialReferenceId: request.context.credentialReferenceId,
        });
        assertPublicationFidelity(published, request, targetRef);
      }

      const readBack = await this.client.readPublication(
        published.nativeId,
        request.context.credentialReferenceId,
      );
      if (readBack === undefined) {
        fail("adapter-protocol", "GitHub publication could not be read back after mutation");
      }
      assertPublicationFidelity(readBack, request, targetRef);
      if (readBack.targetRevision !== published.targetRevision) {
        fail("conflict-stale", "GitHub publication moved before fidelity read-back completed");
      }

      const observedAt = observedNow();
      return {
        value: publicationObservation(readBack),
        references: [publicationReference(readBack, observedAt)],
        observedAt,
        reconciliationToken: `${readBack.nativeId}:${readBack.targetRevision}`,
      };
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async reconcile(request: ReconcileRequest): Promise<ProviderResult<ProviderObservation>> {
    return new GitHubForgeProvider(this.client).reconcile(request);
  }
}

export class GitHubTrustedMerger implements TrustedMerger {
  constructor(private readonly client: GitHubTrustedClient) {}

  async merge(request: TrustedMergeRequest): Promise<ProviderResult<TrustedMergeResult>> {
    try {
      assertTrustedAuthority(request.context);
      assertCandidateIdentity(request.candidateDigest, request.candidateManifest);
      if (request.publicationReference.providerId !== GITHUB_PROVIDER_ID) {
        fail("adapter-protocol", "Trusted merge publication is not owned by GitHub");
      }

      const existing = await this.client.findMergeByIdempotencyKey({
        repositoryKey: request.repositoryKey,
        targetRef: request.expectedTarget.reference,
        expectedTargetRevision: request.expectedTarget.revision,
        expectedBaseRevision: request.expectedBase.revision,
        publicationNativeId: request.publicationReference.nativeId,
        candidateDigest: request.candidateDigest,
        candidateManifest: request.candidateManifest,
        idempotencyKey: request.context.idempotencyKey,
        credentialReferenceId: request.context.credentialReferenceId,
      });
      let merged: GitHubMergeState;
      if (existing !== undefined) {
        assertMergeFidelity(existing, request);
        merged = existing;
      } else {
        const repository = await this.client.inspectRepository(
          request.repositoryKey,
          request.context.credentialReferenceId,
        );
        if (repository.headRevision !== request.expectedTarget.revision) {
          fail(
            "conflict-stale",
            "Repository target moved since trusted merge approval",
            false,
            repositoryReference(repository, observedNow()),
          );
        }
        const publication = await this.client.readPublication(
          request.publicationReference.nativeId,
          request.context.credentialReferenceId,
        );
        if (publication === undefined) {
          fail("missing-resource", "Trusted merge publication no longer exists");
        }
        if (
          publication.repositoryKey !== request.repositoryKey ||
          publication.baseRevision !== request.expectedBase.revision ||
          publication.candidateDigest !== request.candidateDigest ||
          !candidateManifestEquals(publication.candidateManifest, request.candidateManifest)
        ) {
          fail("conflict-stale", "Trusted merge publication candidate identity mismatch");
        }
        merged = await this.client.mergePublication({
          repositoryKey: request.repositoryKey,
          targetRef: request.expectedTarget.reference,
          expectedTargetRevision: request.expectedTarget.revision,
          expectedBaseRevision: request.expectedBase.revision,
          publicationNativeId: request.publicationReference.nativeId,
          candidateDigest: request.candidateDigest,
          candidateManifest: request.candidateManifest,
          idempotencyKey: request.context.idempotencyKey,
          credentialReferenceId: request.context.credentialReferenceId,
        });
        assertMergeFidelity(merged, request);
      }

      const readBack = await this.client.readMerge(
        merged.nativeId,
        request.context.credentialReferenceId,
      );
      if (readBack === undefined) fail("adapter-protocol", "GitHub merge could not be read back");
      assertMergeFidelity(readBack, request);
      if (readBack.resultingRevision !== merged.resultingRevision) {
        fail("conflict-stale", "GitHub merge revision moved during fidelity read-back");
      }
      const observedAt = observedNow();
      const reference = mergeReference(readBack, observedAt);
      return {
        value: {
          changeSetId: request.changeSetId,
          resultingRevision: readBack.resultingRevision,
          mergeReference: reference,
        },
        references: [reference],
        observedAt,
        reconciliationToken: `${readBack.nativeId}:${readBack.resultingRevision}`,
      };
    } catch (error) {
      throw normalizeError(error);
    }
  }

  async reconcile(
    request: TrustedMergeRequest,
  ): Promise<ProviderResult<TrustedMergeReconciliation>> {
    try {
      assertTrustedAuthority(request.context);
      const existing = await this.client.findMergeByIdempotencyKey({
        repositoryKey: request.repositoryKey,
        targetRef: request.expectedTarget.reference,
        expectedTargetRevision: request.expectedTarget.revision,
        expectedBaseRevision: request.expectedBase.revision,
        publicationNativeId: request.publicationReference.nativeId,
        candidateDigest: request.candidateDigest,
        candidateManifest: request.candidateManifest,
        idempotencyKey: request.context.idempotencyKey,
        credentialReferenceId: request.context.credentialReferenceId,
      });
      const observedAt = observedNow();
      if (existing === undefined) {
        return { value: { state: "not-merged" }, references: [], observedAt };
      }
      assertMergeFidelity(existing, request);
      const reference = mergeReference(existing, observedAt);
      return {
        value: {
          state: "merged",
          result: {
            changeSetId: request.changeSetId,
            resultingRevision: existing.resultingRevision,
            mergeReference: reference,
          },
        },
        references: [reference],
        observedAt,
        reconciliationToken: `${existing.nativeId}:${existing.resultingRevision}`,
      };
    } catch (error) {
      throw normalizeError(error);
    }
  }
}

export * from "./client-types.js";
export * from "./http-client.js";
