import type { CIProvider, ProviderObservation, ReconcileRequest } from "@awp/application";
import {
  unsafeOpaqueId,
  type ProviderDescriptor,
  type ProviderId,
  type ProviderOperationContext,
  type ProviderReference,
  type ProviderResult,
} from "@awp/contracts";
import type {
  GitHubActionsTransport,
  GitHubRepositoryConfig,
  GitHubWorkflowRun,
} from "./github.js";

const PROVIDER_ID = unsafeOpaqueId<ProviderId>("github-actions");

export class GitHubRepositoryRegistry {
  private readonly repositories: ReadonlyMap<string, GitHubRepositoryConfig>;

  constructor(repositories: readonly GitHubRepositoryConfig[]) {
    this.repositories = new Map(
      repositories.map((repository) => [repository.repositoryKey, repository]),
    );
  }

  get(repositoryKey: string): GitHubRepositoryConfig {
    const repository = this.repositories.get(repositoryKey);
    if (!repository) throw new Error(`Unknown GitHub CI repository: ${repositoryKey}`);
    return repository;
  }

  find(owner: string, repository: string): GitHubRepositoryConfig | undefined {
    return [...this.repositories.values()].find(
      (entry) => entry.owner === owner && entry.repository === repository,
    );
  }
}

export class GitHubCIProvider implements CIProvider {
  constructor(
    private readonly github: GitHubActionsTransport,
    private readonly repositories: GitHubRepositoryRegistry,
    private readonly sleep: (milliseconds: number) => Promise<void> = (milliseconds) =>
      new Promise((resolve) => setTimeout(resolve, milliseconds)),
    private readonly now: () => string = () => new Date().toISOString(),
    private readonly correlationPolls = 5,
    private readonly correlationPollMs = 500,
  ) {}

  async describe(): Promise<ProviderDescriptor> {
    return {
      providerId: PROVIDER_ID,
      kind: "ci",
      adapterVersion: "i0-v1",
      capabilities: [
        "github-actions.workflow-dispatch",
        "github-actions.cancel",
        "github-actions.reconcile",
        "github-actions.required-checks",
      ],
      authModes: ["github-app-or-fine-grained-token-via-secret-store"],
      resourceTypes: ["github-actions-run", "github-actions-correlation"],
      healthFeatures: ["workflow-run-readback", "required-check-readback"],
    };
  }

  async start(
    context: ProviderOperationContext,
    repositoryKey: string,
    candidateDigest: string,
  ): Promise<ProviderResult<ProviderObservation>> {
    validateCandidateDigest(candidateDigest);
    const repository = this.repositories.get(repositoryKey);
    const operationKey = operationKeyFor(context.operationId, candidateDigest);

    const existing = await this.findCorrelatedRun(repository, context, operationKey);
    if (existing) return this.runResult(repository, existing, candidateDigest);

    try {
      await this.github.dispatch(
        repository,
        context.credentialReferenceId,
        operationKey,
        candidateDigest,
      );
    } catch (error) {
      // A request timeout after GitHub accepted workflow_dispatch is ambiguous.
      // Reconcile by the run-name operation key before exposing failure upstream.
      const reconciled = await this.findCorrelatedRun(repository, context, operationKey);
      if (reconciled) return this.runResult(repository, reconciled, candidateDigest);
      throw error;
    }

    for (let attempt = 0; attempt < this.correlationPolls; attempt += 1) {
      const run = await this.findCorrelatedRun(repository, context, operationKey);
      if (run) return this.runResult(repository, run, candidateDigest);
      if (attempt + 1 < this.correlationPolls) await this.sleep(this.correlationPollMs);
    }

    return this.correlationResult(repository, operationKey, candidateDigest);
  }

  async cancel(
    context: ProviderOperationContext,
    reference: ProviderReference,
  ): Promise<ProviderResult<ProviderObservation>> {
    validateReference(reference);
    const parsed = parseReference(reference);
    const repository = this.repositories.find(parsed.owner, parsed.repository);
    if (!repository) throw new Error("GitHub CI reference points to an unconfigured repository");

    if (parsed.kind === "correlation") {
      const run = await this.findCorrelatedRun(repository, context, parsed.nativeId);
      if (!run) {
        return this.correlationObservation(reference, "cancel-pending-correlation");
      }
      await this.github.cancelRun(repository, context.credentialReferenceId, run.id);
      const current = await this.github.getRun(repository, context.credentialReferenceId, run.id);
      return current
        ? this.runResult(repository, current, parsed.candidateDigest)
        : this.missingRunResult(reference);
    }

    await this.github.cancelRun(repository, context.credentialReferenceId, parsed.runId);
    const current = await this.github.getRun(
      repository,
      context.credentialReferenceId,
      parsed.runId,
    );
    return current
      ? this.runResult(repository, current, parsed.candidateDigest)
      : this.missingRunResult(reference);
  }

  async reconcile(request: ReconcileRequest): Promise<ProviderResult<ProviderObservation>> {
    validateReference(request.reference);
    const parsed = parseReference(request.reference);
    const repository = this.repositories.find(parsed.owner, parsed.repository);
    if (!repository) throw new Error("GitHub CI reference points to an unconfigured repository");

    if (parsed.kind === "correlation") {
      const run = await this.findCorrelatedRun(repository, request.context, parsed.nativeId);
      return run
        ? this.runResult(repository, run, parsed.candidateDigest)
        : this.correlationObservation(request.reference, "dispatched-awaiting-correlation");
    }

    const run = await this.github.getRun(
      repository,
      request.context.credentialReferenceId,
      parsed.runId,
    );
    return run
      ? this.runResult(repository, run, parsed.candidateDigest)
      : this.missingRunResult(request.reference);
  }

  async listRequiredChecks(
    context: ProviderOperationContext,
    repositoryKey: string,
    branch: string,
  ): Promise<readonly string[]> {
    const repository = this.repositories.get(repositoryKey);
    return this.github.requiredChecks(repository, context.credentialReferenceId, branch);
  }

  private async findCorrelatedRun(
    repository: GitHubRepositoryConfig,
    context: ProviderOperationContext,
    operationKey: string,
  ): Promise<GitHubWorkflowRun | undefined> {
    const runs = await this.github.listRuns(repository, context.credentialReferenceId);
    const matches = runs.filter((run) => run.displayTitle === operationKey);
    if (matches.length > 1) {
      throw new Error(`Ambiguous GitHub Actions correlation for ${operationKey}`);
    }
    return matches[0];
  }

  private runResult(
    repository: GitHubRepositoryConfig,
    run: GitHubWorkflowRun,
    candidateDigest: string,
  ): ProviderResult<ProviderObservation> {
    const observedAt = run.updatedAt ?? this.now();
    const reference = runReference(repository, run, candidateDigest, observedAt);
    return {
      value: {
        state: normalizeRunState(run),
        details: {
          repositoryKey: repository.repositoryKey,
          workflowId: repository.workflowId,
          runId: run.id,
          providerStatus: run.status,
          providerConclusion: run.conclusion ?? null,
          runAttempt: run.runAttempt ?? 1,
          candidateDigest,
        },
      },
      references: [reference],
      observedAt,
      reconciliationToken: `${run.id}:${run.runAttempt ?? 1}:${run.status}:${run.conclusion ?? ""}`,
    };
  }

  private correlationResult(
    repository: GitHubRepositoryConfig,
    operationKey: string,
    candidateDigest: string,
  ): ProviderResult<ProviderObservation> {
    const observedAt = this.now();
    const reference = correlationReference(repository, operationKey, candidateDigest, observedAt);
    return {
      value: {
        state: "dispatched-awaiting-correlation",
        details: {
          repositoryKey: repository.repositoryKey,
          workflowId: repository.workflowId,
          operationKey,
          candidateDigest,
        },
      },
      references: [reference],
      observedAt,
      reconciliationToken: operationKey,
    };
  }

  private correlationObservation(
    reference: ProviderReference,
    state: string,
  ): ProviderResult<ProviderObservation> {
    const observedAt = this.now();
    return {
      value: { state, details: { correlation: reference.nativeId } },
      references: [reference],
      observedAt,
      reconciliationToken: reference.nativeId,
    };
  }

  private missingRunResult(reference: ProviderReference): ProviderResult<ProviderObservation> {
    const observedAt = this.now();
    return {
      value: { state: "missing", details: { nativeId: reference.nativeId } },
      references: [reference],
      observedAt,
      reconciliationToken: `${reference.nativeId}:missing`,
    };
  }
}

export function operationKeyFor(operationId: string, candidateDigest: string): string {
  return `awp:${operationId}:${candidateDigest.slice(0, 12)}`;
}

function runReference(
  repository: GitHubRepositoryConfig,
  run: GitHubWorkflowRun,
  candidateDigest: string,
  observedAt: string,
): ProviderReference {
  return {
    providerId: PROVIDER_ID,
    resourceType: "github-actions-run",
    nativeId: String(run.id),
    nativeRevision: String(run.runAttempt ?? 1),
    observedAt,
    // Repository/candidate identity is encoded in the provider URL so reconcile can
    // reject cross-repository references without putting GitHub types in AWP ports.
    url: referenceUrl(repository, "run", String(run.id), candidateDigest),
  };
}

function correlationReference(
  repository: GitHubRepositoryConfig,
  operationKey: string,
  candidateDigest: string,
  observedAt: string,
): ProviderReference {
  return {
    providerId: PROVIDER_ID,
    resourceType: "github-actions-correlation",
    nativeId: operationKey,
    url: referenceUrl(repository, "correlation", operationKey, candidateDigest),
    observedAt,
  };
}

function referenceUrl(
  repository: GitHubRepositoryConfig,
  kind: "run" | "correlation",
  nativeId: string,
  candidateDigest: string,
): string {
  const url = new URL(`https://github.com/${repository.owner}/${repository.repository}/actions`);
  url.searchParams.set("awpKind", kind);
  url.searchParams.set("awpNativeId", nativeId);
  url.searchParams.set("awpCandidateDigest", candidateDigest);
  return url.toString();
}

function validateReference(reference: ProviderReference): void {
  if (
    reference.providerId !== PROVIDER_ID ||
    (reference.resourceType !== "github-actions-run" &&
      reference.resourceType !== "github-actions-correlation")
  ) {
    throw new Error("GitHub CI reconcile received a foreign provider reference");
  }
}

type ParsedReference =
  | {
      readonly kind: "run";
      readonly owner: string;
      readonly repository: string;
      readonly nativeId: string;
      readonly runId: number;
      readonly candidateDigest: string;
    }
  | {
      readonly kind: "correlation";
      readonly owner: string;
      readonly repository: string;
      readonly nativeId: string;
      readonly candidateDigest: string;
    };

function parseReference(reference: ProviderReference): ParsedReference {
  const url = new URL(reference.url ?? "https://invalid.invalid/");
  const [owner, repository] = url.pathname.split("/").filter(Boolean);
  const candidateDigest = url.searchParams.get("awpCandidateDigest");
  const kind = url.searchParams.get("awpKind");
  if (!owner || !repository || !candidateDigest) throw new Error("Invalid GitHub CI reference");
  if (kind === "run") {
    const runId = Number(reference.nativeId);
    if (!Number.isSafeInteger(runId) || runId <= 0)
      throw new Error("Invalid GitHub Actions run ID");
    return { kind, owner, repository, nativeId: reference.nativeId, runId, candidateDigest };
  }
  if (kind === "correlation") {
    return { kind, owner, repository, nativeId: reference.nativeId, candidateDigest };
  }
  throw new Error("Invalid GitHub CI reference kind");
}

function normalizeRunState(run: GitHubWorkflowRun): string {
  if (run.status !== "completed") return run.status === "queued" ? "queued" : "running";
  switch (run.conclusion) {
    case "success":
      return "succeeded";
    case "cancelled":
      return "cancelled";
    case "skipped":
    case "neutral":
      return "completed-non-success";
    default:
      return "failed";
  }
}

function validateCandidateDigest(candidateDigest: string): void {
  if (!/^[a-f0-9]{7,64}$/i.test(candidateDigest)) {
    throw new Error("GitHub CI candidate digest must be a Git-style hexadecimal revision");
  }
}
