import { App, type Octokit } from "octokit";
import { env } from "../env.server";
import { prisma } from "~/db.server";
import { logger } from "./logger.server";
import { errAsync, fromPromise, okAsync, type ResultAsync } from "neverthrow";

export const githubApp =
  env.GITHUB_APP_ENABLED === "1"
    ? new App({
        appId: env.GITHUB_APP_ID,
        privateKey: env.GITHUB_APP_PRIVATE_KEY,
        webhooks: {
          secret: env.GITHUB_APP_WEBHOOK_SECRET,
        },
      })
    : null;

/**
 * Links a GitHub App installation to a Trigger organization
 */
export async function linkGitHubAppInstallation(
  installationId: number,
  organizationId: string
): Promise<void> {
  if (!githubApp) {
    throw new Error("GitHub App is not enabled");
  }

  const octokit = await githubApp.getInstallationOctokit(installationId);
  const { data: installation } = await octokit.rest.apps.getInstallation({
    installation_id: installationId,
  });

  const repositories = await fetchInstallationRepositories(octokit, installationId);

  const repositorySelection = installation.repository_selection === "all" ? "ALL" : "SELECTED";

  await prisma.githubAppInstallation.create({
    data: {
      appInstallationId: installationId,
      organizationId,
      targetId: installation.target_id,
      targetType: installation.target_type,
      accountHandle: installation.account
        ? "login" in installation.account
          ? installation.account.login
          : "slug" in installation.account
          ? installation.account.slug
          : "-"
        : "-",
      permissions: installation.permissions,
      repositorySelection,
      repositories: {
        create: repositories,
      },
    },
  });
}

/**
 * Links a GitHub App installation to a Trigger organization
 */
export async function updateGitHubAppInstallation(installationId: number): Promise<void> {
  if (!githubApp) {
    throw new Error("GitHub App is not enabled");
  }

  const octokit = await githubApp.getInstallationOctokit(installationId);
  const { data: installation } = await octokit.rest.apps.getInstallation({
    installation_id: installationId,
  });

  const existingInstallation = await prisma.githubAppInstallation.findFirst({
    where: { appInstallationId: installationId },
  });

  if (!existingInstallation) {
    throw new Error("GitHub App installation not found");
  }

  const repositorySelection = installation.repository_selection === "all" ? "ALL" : "SELECTED";

  // repos are updated asynchronously via webhook events
  await prisma.githubAppInstallation.update({
    where: { id: existingInstallation?.id },
    data: {
      appInstallationId: installationId,
      targetId: installation.target_id,
      targetType: installation.target_type,
      accountHandle: installation.account
        ? "login" in installation.account
          ? installation.account.login
          : "slug" in installation.account
          ? installation.account.slug
          : "-"
        : "-",
      permissions: installation.permissions,
      suspendedAt: existingInstallation?.suspendedAt,
      repositorySelection,
    },
  });
}

async function fetchInstallationRepositories(octokit: Octokit, installationId: number) {
  const iterator = octokit.paginate.iterator(octokit.rest.apps.listReposAccessibleToInstallation, {
    installation_id: installationId,
    per_page: 100,
  });

  const allRepos = [];
  const maxPages = 3;
  let pageCount = 0;

  for await (const { data } of iterator) {
    pageCount++;
    allRepos.push(...data);

    if (maxPages && pageCount >= maxPages) {
      logger.warn("GitHub installation repository fetch truncated", {
        installationId,
        maxPages,
        totalReposFetched: allRepos.length,
      });
      break;
    }
  }

  return allRepos.map((repo) => ({
    githubId: repo.id,
    name: repo.name,
    fullName: repo.full_name,
    htmlUrl: repo.html_url,
    private: repo.private,
    defaultBranch: repo.default_branch,
  }));
}

/**
 * Checks if a branch exists in a GitHub repository
 */
export function checkGitHubBranchExists(
  installationId: number,
  fullRepoName: string,
  branch: string
): ResultAsync<boolean, { type: "other" | "github_app_not_enabled"; cause?: unknown }> {
  if (!githubApp) {
    return errAsync({ type: "github_app_not_enabled" as const });
  }

  if (!branch || branch.trim() === "") {
    return okAsync(false);
  }

  const [owner, repo] = fullRepoName.split("/");

  const getOctokit = () =>
    fromPromise(githubApp.getInstallationOctokit(installationId), (error) => ({
      type: "other" as const,
      cause: error,
    }));

  const getBranch = (octokit: Octokit) =>
    fromPromise(
      octokit.rest.repos.getBranch({
        owner,
        repo,
        branch,
      }),
      (error) => ({
        type: "other" as const,
        cause: error,
      })
    );

  return getOctokit()
    .andThen((octokit) => getBranch(octokit))
    .map(() => true)
    .orElse((error) => {
      if (
        error.cause &&
        error.cause instanceof Error &&
        "status" in error.cause &&
        error.cause.status === 404
      ) {
        return okAsync(false);
      }

      return errAsync(error);
    });
}
