import { ProviderFailure } from "@awp/contracts";
import type { ProviderResult } from "@awp/contracts";
import { assertCandidateIdentity, candidateManifestEquals } from "@awp/domain";
import type { ChangeSet } from "@awp/domain";
import type {
  RepositoryRevisionIdentity,
  TrustedMerger,
  TrustedMergeRequest,
  TrustedMergeResult,
} from "./ports/providers.js";

export class TrustedMergePreconditionError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "TrustedMergePreconditionError";
  }
}

function sameIdentity(
  left: RepositoryRevisionIdentity,
  right: RepositoryRevisionIdentity,
): boolean {
  return left.reference === right.reference && left.revision === right.revision;
}

export function assertTrustedMergePreconditions(
  changeSet: ChangeSet,
  request: TrustedMergeRequest,
  observedTarget: RepositoryRevisionIdentity,
): void {
  assertCandidateIdentity(request.candidateDigest, request.candidateManifest);
  if (changeSet.id !== request.changeSetId) {
    throw new TrustedMergePreconditionError("Trusted merge ChangeSet identity mismatch");
  }
  if (
    changeSet.candidateDigest !== request.candidateDigest ||
    !candidateManifestEquals(changeSet.candidateManifest, request.candidateManifest)
  ) {
    throw new TrustedMergePreconditionError("Trusted merge candidate identity mismatch");
  }
  if (changeSet.baseIdentity !== request.expectedBase.revision) {
    throw new TrustedMergePreconditionError("Trusted merge base identity mismatch");
  }
  if (!sameIdentity(observedTarget, request.expectedTarget)) {
    throw new TrustedMergePreconditionError("Trusted merge target identity mismatch");
  }
}

function ambiguousFailure(failure: ProviderFailure): boolean {
  if (!failure.providerError.retryable) return false;
  return ["unavailable", "rate-capacity", "adapter-protocol"].includes(
    failure.providerError.category,
  );
}

/**
 * Executes one trusted merge mutation. If the provider response is ambiguous,
 * reconciliation of the exact immutable request happens before any caller can
 * decide to retry the mutation.
 */
export async function executeTrustedMerge(
  merger: TrustedMerger,
  changeSet: ChangeSet,
  request: TrustedMergeRequest,
  observeTarget: () => Promise<RepositoryRevisionIdentity>,
): Promise<ProviderResult<TrustedMergeResult>> {
  // Validate immutable request identity before consulting the provider, but do not
  // require the mutable branch head to still equal the pre-merge target until we
  // have reconciled whether this exact logical merge already completed.
  assertTrustedMergePreconditions(changeSet, request, request.expectedTarget);
  const existing = await merger.reconcile(request);
  if (existing.value.state === "merged") {
    return {
      ...existing,
      value: existing.value.result,
    };
  }
  const observedTarget = await observeTarget();
  assertTrustedMergePreconditions(changeSet, request, observedTarget);
  try {
    return await merger.merge(request);
  } catch (error) {
    if (!(error instanceof ProviderFailure) || !ambiguousFailure(error)) throw error;
    const reconciliation = await merger.reconcile(request);
    if (reconciliation.value.state === "merged") {
      return {
        ...reconciliation,
        value: reconciliation.value.result,
      };
    }
    throw error;
  }
}
