'use node';

import { createSign } from 'node:crypto';
import { getAuthUserId } from '@convex-dev/auth/server';
import { ConvexError, v } from 'convex/values';

import type { Id } from './_generated/dataModel';
import type { ActionCtx } from './_generated/server';
import { internal } from './_generated/api';
import { action } from './_generated/server';

type GitHubInstallationAccount = {
  id?: number;
  login?: string;
  avatar_url?: string;
};

type GitHubInstallation = {
  id: number;
  account?: GitHubInstallationAccount;
};

type GitHubRepository = {
  id: number;
  name: string;
  full_name: string;
  private: boolean;
  fork: boolean;
  html_url: string;
  default_branch: string;
  owner: {
    login: string;
  };
  description?: string | null;
};

type GitHubListRepositoriesResponse = {
  repositories: GitHubRepository[];
};

const base64Url = (value: string | Buffer) =>
  Buffer.from(value)
    .toString('base64')
    .replaceAll('+', '-')
    .replaceAll('/', '_')
    .replaceAll('=', '');

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');

const firstText = (...values: (string | null | undefined)[]) =>
  values.map((value) => value?.trim()).find((value) => value);

const createGitHubAppJwt = () => {
  const appId = getEnv('GITHUB_APP_ID');
  const privateKey = normalizePrivateKey(getEnv('GITHUB_APP_PRIVATE_KEY'));
  const now = Math.floor(Date.now() / 1000);
  const header = base64Url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
  const payload = base64Url(
    JSON.stringify({
      iat: now - 60,
      exp: now + 9 * 60,
      iss: appId,
    }),
  );
  const body = `${header}.${payload}`;
  const signature = createSign('RSA-SHA256').update(body).sign(privateKey);
  return `${body}.${base64Url(signature)}`;
};

const githubFetch = async <T>(
  path: string,
  token: string,
  init: RequestInit = {},
): Promise<T> => {
  const response = await fetch(`https://api.github.com${path}`, {
    ...init,
    headers: {
      Accept: 'application/vnd.github+json',
      Authorization: `Bearer ${token}`,
      'User-Agent': 'Spoon',
      'X-GitHub-Api-Version': '2022-11-28',
      ...init.headers,
    },
  });

  if (!response.ok) {
    const body = await response.text();
    throw new ConvexError(
      `GitHub API request failed (${response.status}): ${body}`,
    );
  }

  return (await response.json()) as T;
};

const createInstallationToken = async (installationId: string) => {
  const jwt = createGitHubAppJwt();
  const result = await githubFetch<{ token: string; expires_at: string }>(
    `/app/installations/${installationId}/access_tokens`,
    jwt,
    { method: 'POST' },
  );
  return result.token;
};

const getRequiredUserId = async (ctx: ActionCtx) => {
  const userId = await getAuthUserId(ctx);
  if (!userId) throw new ConvexError('Not authenticated.');
  return userId;
};

export const syncConfiguredInstallation = action({
  args: {},
  handler: async (ctx): Promise<Id<'gitConnections'>> => {
    const userId = await getRequiredUserId(ctx);
    const installationId = getEnv('GITHUB_APP_INSTALLATION_ID');
    const jwt = createGitHubAppJwt();
    const installation = await githubFetch<GitHubInstallation>(
      `/app/installations/${installationId}`,
      jwt,
    );
    const account = installation.account;
    const displayName =
      account?.login ?? `GitHub installation ${installationId}`;

    return await ctx.runMutation(internal.github.upsertConnectionForUser, {
      userId,
      providerAccountId: account?.id?.toString(),
      displayName,
      username: account?.login,
      avatarUrl: account?.avatar_url,
      installationId: installation.id.toString(),
    });
  },
});

export const listInstallationRepositories = action({
  args: {},
  handler: async (
    ctx,
  ): Promise<
    {
      id: number;
      name: string;
      fullName: string;
      owner: string;
      private: boolean;
      fork: boolean;
      url: string;
      defaultBranch: string;
      description?: string;
    }[]
  > => {
    const userId = await getRequiredUserId(ctx);
    const connection = await ctx.runQuery(
      internal.github.getConnectionForUser,
      {
        userId,
      },
    );
    if (!connection?.installationId) {
      throw new ConvexError('Connect a GitHub App installation first.');
    }

    const token = await createInstallationToken(connection.installationId);
    const result = await githubFetch<GitHubListRepositoriesResponse>(
      '/installation/repositories?per_page=100',
      token,
    );

    return result.repositories.map((repo) => ({
      id: repo.id,
      name: repo.name,
      fullName: repo.full_name,
      owner: repo.owner.login,
      private: repo.private,
      fork: repo.fork,
      url: repo.html_url,
      defaultBranch: repo.default_branch,
      description: repo.description ?? undefined,
    }));
  },
});

export const createFork = action({
  args: {
    upstreamOwner: v.string(),
    upstreamRepo: v.string(),
    name: v.optional(v.string()),
    description: v.optional(v.string()),
    organization: v.optional(v.string()),
  },
  handler: async (ctx, args): Promise<Id<'spoons'>> => {
    const userId = await getRequiredUserId(ctx);
    const connection = await ctx.runQuery(
      internal.github.getConnectionForUser,
      {
        userId,
      },
    );
    if (!connection?.installationId) {
      throw new ConvexError('Connect a GitHub App installation first.');
    }

    const upstreamOwner = args.upstreamOwner.trim();
    const upstreamRepo = args.upstreamRepo.trim();
    if (!upstreamOwner || !upstreamRepo) {
      throw new ConvexError('Upstream owner and repository are required.');
    }

    const token = await createInstallationToken(connection.installationId);
    const upstream = await githubFetch<GitHubRepository>(
      `/repos/${encodeURIComponent(upstreamOwner)}/${encodeURIComponent(
        upstreamRepo,
      )}`,
      token,
    );

    const body: Record<string, string | boolean> = {
      default_branch_only: false,
    };
    const forkName = args.name?.trim();
    if (forkName) body.name = forkName;
    const organization = args.organization?.trim();
    if (organization) body.organization = organization;

    const fork = await githubFetch<GitHubRepository>(
      `/repos/${encodeURIComponent(upstreamOwner)}/${encodeURIComponent(
        upstreamRepo,
      )}/forks`,
      token,
      {
        method: 'POST',
        body: JSON.stringify(body),
      },
    );
    const description = firstText(args.description, upstream.description);

    return await ctx.runMutation(internal.github.createForkSpoonRecord, {
      ownerId: userId,
      name: fork.name,
      description,
      upstreamOwner,
      upstreamRepo,
      upstreamDefaultBranch: upstream.default_branch,
      upstreamUrl: upstream.html_url,
      forkOwner: fork.owner.login,
      forkRepo: fork.name,
      forkDefaultBranch: fork.default_branch,
      forkUrl: fork.html_url,
      visibility: fork.private ? 'private' : 'public',
      connectionId: connection._id,
    });
  },
});
