import type { CredentialReferenceId } from "@awp/contracts";

export interface GitHubRepositoryConfig {
  readonly repositoryKey: string;
  readonly owner: string;
  readonly repository: string;
  readonly workflowId: string;
  readonly dispatchRef: string;
}

export interface GitHubWorkflowRun {
  readonly id: number;
  readonly name: string;
  readonly displayTitle: string;
  readonly status: "queued" | "in_progress" | "completed" | string;
  readonly conclusion?: string | null;
  readonly htmlUrl?: string;
  readonly headSha?: string;
  readonly runAttempt?: number;
  readonly updatedAt?: string;
}

export interface GitHubActionsTransport {
  dispatch(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    operationKey: string,
    candidateDigest: string,
  ): Promise<void>;
  listRuns(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
  ): Promise<readonly GitHubWorkflowRun[]>;
  getRun(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    runId: number,
  ): Promise<GitHubWorkflowRun | undefined>;
  cancelRun(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    runId: number,
  ): Promise<void>;
  requiredChecks(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    branch: string,
  ): Promise<readonly string[]>;
}

export interface GitHubCredentialLease {
  readonly token: string;
  dispose(): void;
}

export interface GitHubCredentialResolver {
  resolve(referenceId: CredentialReferenceId, purpose: string): Promise<GitHubCredentialLease>;
}

interface FetchResponse {
  readonly ok: boolean;
  readonly status: number;
  readonly statusText: string;
  text(): Promise<string>;
}

export type GitHubFetch = (
  url: string,
  init: {
    method: string;
    headers: Readonly<Record<string, string>>;
    body?: string;
  },
) => Promise<FetchResponse>;

export class GitHubActionsHttpTransport implements GitHubActionsTransport {
  constructor(
    private readonly credentials: GitHubCredentialResolver,
    private readonly fetcher: GitHubFetch = globalThis.fetch as unknown as GitHubFetch,
    private readonly apiBase = "https://api.github.com",
  ) {}

  async dispatch(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    operationKey: string,
    candidateDigest: string,
  ): Promise<void> {
    await this.request(
      credentialReferenceId,
      "ci.dispatch",
      "POST",
      `${this.repoUrl(config)}/actions/workflows/${encodeURIComponent(config.workflowId)}/dispatches`,
      {
        ref: config.dispatchRef,
        inputs: {
          awp_operation_key: operationKey,
          awp_candidate_digest: candidateDigest,
        },
      },
      [204],
    );
  }

  async listRuns(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
  ): Promise<readonly GitHubWorkflowRun[]> {
    const response = await this.request(
      credentialReferenceId,
      "ci.reconcile",
      "GET",
      `${this.repoUrl(config)}/actions/workflows/${encodeURIComponent(config.workflowId)}/runs?event=workflow_dispatch&per_page=50`,
      undefined,
      [200],
    );
    const parsed = parseObject(response);
    const runs = Array.isArray(parsed.workflow_runs) ? parsed.workflow_runs : [];
    return runs.flatMap((run) => {
      const mapped = parseRun(run);
      return mapped ? [mapped] : [];
    });
  }

  async getRun(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    runId: number,
  ): Promise<GitHubWorkflowRun | undefined> {
    try {
      const response = await this.request(
        credentialReferenceId,
        "ci.reconcile",
        "GET",
        `${this.repoUrl(config)}/actions/runs/${runId}`,
        undefined,
        [200],
      );
      return parseRun(JSON.parse(response));
    } catch (error) {
      if (error instanceof GitHubHttpError && error.status === 404) return undefined;
      throw error;
    }
  }

  async cancelRun(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    runId: number,
  ): Promise<void> {
    await this.request(
      credentialReferenceId,
      "ci.cancel",
      "POST",
      `${this.repoUrl(config)}/actions/runs/${runId}/cancel`,
      undefined,
      [202, 409],
    );
  }

  async requiredChecks(
    config: GitHubRepositoryConfig,
    credentialReferenceId: CredentialReferenceId,
    branch: string,
  ): Promise<readonly string[]> {
    try {
      const response = await this.request(
        credentialReferenceId,
        "ci.required-checks.read",
        "GET",
        `${this.repoUrl(config)}/branches/${encodeURIComponent(branch)}/protection/required_status_checks`,
        undefined,
        [200],
      );
      const parsed = parseObject(response);
      const contexts = Array.isArray(parsed.contexts)
        ? parsed.contexts.filter((value): value is string => typeof value === "string")
        : [];
      const checks = Array.isArray(parsed.checks)
        ? parsed.checks.flatMap((value) => {
            const object =
              typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
            return typeof object.context === "string" ? [object.context] : [];
          })
        : [];
      return [...new Set([...contexts, ...checks])].sort();
    } catch (error) {
      if (error instanceof GitHubHttpError && error.status === 404) return [];
      throw error;
    }
  }

  private repoUrl(config: GitHubRepositoryConfig): string {
    return `${this.apiBase}/repos/${encodeURIComponent(config.owner)}/${encodeURIComponent(config.repository)}`;
  }

  private async request(
    credentialReferenceId: CredentialReferenceId,
    purpose: string,
    method: string,
    url: string,
    body: unknown,
    acceptedStatuses: readonly number[],
  ): Promise<string> {
    const lease = await this.credentials.resolve(credentialReferenceId, purpose);
    try {
      const response = await this.fetcher(url, {
        method,
        headers: {
          Accept: "application/vnd.github+json",
          Authorization: `Bearer ${lease.token}`,
          "X-GitHub-Api-Version": "2022-11-28",
          "Content-Type": "application/json",
        },
        ...(body === undefined ? {} : { body: JSON.stringify(body) }),
      });
      const text = await response.text();
      if (!response.ok && !acceptedStatuses.includes(response.status)) {
        throw new GitHubHttpError(response.status, safeGitHubMessage(response.status, text));
      }
      if (!acceptedStatuses.includes(response.status)) {
        throw new GitHubHttpError(
          response.status,
          `Unexpected GitHub API status ${response.status}`,
        );
      }
      return text;
    } finally {
      lease.dispose();
    }
  }
}

export class GitHubHttpError extends Error {
  constructor(
    readonly status: number,
    message: string,
  ) {
    super(message);
    this.name = "GitHubHttpError";
  }
}

function parseObject(source: string): Readonly<Record<string, unknown>> {
  if (!source) return {};
  const parsed: unknown = JSON.parse(source);
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
    ? (parsed as Readonly<Record<string, unknown>>)
    : {};
}

function parseRun(value: unknown): GitHubWorkflowRun | undefined {
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
  const run = value as Record<string, unknown>;
  if (
    typeof run.id !== "number" ||
    typeof run.name !== "string" ||
    typeof run.status !== "string"
  ) {
    return undefined;
  }
  return {
    id: run.id,
    name: run.name,
    displayTitle: typeof run.display_title === "string" ? run.display_title : run.name,
    status: run.status,
    ...(typeof run.conclusion === "string" || run.conclusion === null
      ? { conclusion: run.conclusion }
      : {}),
    ...(typeof run.html_url === "string" ? { htmlUrl: run.html_url } : {}),
    ...(typeof run.head_sha === "string" ? { headSha: run.head_sha } : {}),
    ...(typeof run.run_attempt === "number" ? { runAttempt: run.run_attempt } : {}),
    ...(typeof run.updated_at === "string" ? { updatedAt: run.updated_at } : {}),
  };
}

function safeGitHubMessage(status: number, body: string): string {
  try {
    const parsed = parseObject(body);
    if (typeof parsed.message === "string") return `GitHub API ${status}: ${parsed.message}`;
  } catch {
    // Fall through to a body-free safe message; provider responses may contain details we must not echo.
  }
  return `GitHub API request failed with status ${status}`;
}
