import { createAppAuth } from '@octokit/auth-app';
import { Octokit } from '@octokit/rest';
import { ConvexError } from 'convex/values';

import type { Doc } from './_generated/dataModel';

export type GitHubCommitSummary = {
  sha: string;
  message: string;
  authorName?: string;
  authorEmail?: string;
  authorLogin?: string;
  committedAt?: number;
  htmlUrl?: string;
  filesChanged?: number;
  additions?: number;
  deletions?: number;
};

export type GitHubPullRequestSummary = {
  githubId: number;
  number: number;
  repoFullName: string;
  title: string;
  state: 'open' | 'closed' | 'merged';
  draft: boolean;
  authorLogin?: string;
  baseRef: string;
  headRef: string;
  headRepoFullName?: string;
  htmlUrl: string;
  createdAtGithub?: number;
  updatedAtGithub?: number;
  mergedAtGithub?: number;
};

export type GitHubCompareSummary = {
  aheadBy: number;
  mergeBaseSha?: string;
  headSha?: string;
  baseSha?: string;
  htmlUrl?: string;
  commits: GitHubCommitSummary[];
};

const getEnv = (name: string) => {
  const value = process.env[name]?.trim();
  if (!value) throw new ConvexError(`${name} is not configured.`);
  return value;
};

const normalizePrivateKey = (value: string) => value.replaceAll('\\n', '\n');

export const getInstallationOctokit = (installationId: string) =>
  new Octokit({
    authStrategy: createAppAuth,
    auth: {
      appId: getEnv('GITHUB_APP_ID'),
      privateKey: normalizePrivateKey(getEnv('GITHUB_APP_PRIVATE_KEY')),
      installationId,
    },
    userAgent: 'Spoon',
    request: {
      headers: {
        'X-GitHub-Api-Version': '2022-11-28',
      },
    },
  });

export const getSpoonInstallationId = (
  spoon: Doc<'spoons'>,
  connection?: Doc<'gitConnections'> | null,
) => {
  const installationId =
    spoon.githubInstallationId ?? connection?.installationId ?? undefined;
  if (!installationId) {
    throw new ConvexError('Connect a GitHub App installation first.');
  }
  return installationId;
};

export const getRepository = async (
  octokit: Octokit,
  owner: string,
  repo: string,
) => {
  const result = await octokit.rest.repos.get({ owner, repo });
  return result.data;
};

const toMillis = (value?: string | null) =>
  value ? new Date(value).getTime() : undefined;

const normalizeCompareCommit = (
  commit: Awaited<
    ReturnType<Octokit['rest']['repos']['compareCommitsWithBasehead']>
  >['data']['commits'][number],
): GitHubCommitSummary => ({
  sha: commit.sha,
  message: commit.commit.message,
  authorName: commit.commit.author?.name ?? undefined,
  authorEmail: commit.commit.author?.email ?? undefined,
  authorLogin: commit.author?.login ?? undefined,
  committedAt: toMillis(
    commit.commit.author?.date ?? commit.commit.committer?.date,
  ),
  htmlUrl: commit.html_url,
});

export const compareAcrossForkNetwork = async (
  octokit: Octokit,
  args: {
    owner: string;
    repo: string;
    baseOwner: string;
    baseBranch: string;
    headOwner: string;
    headBranch: string;
  },
): Promise<GitHubCompareSummary> => {
  const basehead = `${args.baseOwner}:${args.baseBranch}...${args.headOwner}:${args.headBranch}`;
  const result = await octokit.rest.repos.compareCommitsWithBasehead({
    owner: args.owner,
    repo: args.repo,
    basehead,
    per_page: 100,
  });
  const commits = result.data.commits.map(normalizeCompareCommit);
  return {
    aheadBy: result.data.ahead_by,
    mergeBaseSha: result.data.merge_base_commit.sha,
    headSha: commits[commits.length - 1]?.sha,
    baseSha: result.data.base_commit.sha,
    htmlUrl: result.data.html_url,
    commits,
  };
};

const normalizePullRequest = (
  repoFullName: string,
  pull: Awaited<ReturnType<Octokit['rest']['pulls']['list']>>['data'][number],
): GitHubPullRequestSummary => ({
  githubId: pull.id,
  number: pull.number,
  repoFullName,
  title: pull.title,
  state: pull.merged_at ? 'merged' : pull.state === 'open' ? 'open' : 'closed',
  draft: pull.draft === true,
  authorLogin: pull.user?.login ?? undefined,
  baseRef: pull.base.ref,
  headRef: pull.head.ref,
  headRepoFullName: pull.head.repo.full_name,
  htmlUrl: pull.html_url,
  createdAtGithub: toMillis(pull.created_at),
  updatedAtGithub: toMillis(pull.updated_at),
  mergedAtGithub: toMillis(pull.merged_at),
});

export const listPullRequests = async (
  octokit: Octokit,
  args: { owner: string; repo: string; head?: string },
) => {
  const result = await octokit.rest.pulls.list({
    owner: args.owner,
    repo: args.repo,
    state: 'all',
    per_page: 100,
    head: args.head,
  });
  return result.data.map((pull) =>
    normalizePullRequest(`${args.owner}/${args.repo}`, pull),
  );
};

export const syncForkBranch = async (
  octokit: Octokit,
  args: { forkOwner: string; forkRepo: string; branch: string },
) => {
  const result = await octokit.rest.repos.mergeUpstream({
    owner: args.forkOwner,
    repo: args.forkRepo,
    branch: args.branch,
  });
  return result.data;
};
