import { ConvexError, v } from "convex/values";
import { z } from "zod";

import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import { action, internalMutation, query } from "./_generated/server";
import { createInitialArtifacts } from "./artifactModel";
import { requireOwnerId } from "./authz";

const gitHubRepositorySchema = z.object({
  default_branch: z.string(),
  description: z.string().nullable(),
  html_url: z.url(),
  id: z.number(),
  name: z.string(),
  owner: z.object({ login: z.string() }),
});

const parseRepository = (value: string): { owner: string; repo: string } => {
  const normalized = value.trim().replace(/\.git$/u, "");
  const match = normalized.match(
    /^(?:https?:\/\/github\.com\/)?(?<owner>[^/\s]+)\/(?<repo>[^/\s]+)$/iu
  );
  if (!match?.groups?.owner || !match.groups.repo) {
    throw new ConvexError("Use a GitHub repository in owner/name format");
  }
  return { owner: match.groups.owner, repo: match.groups.repo };
};

const readGitHubRepository = (value: unknown) => {
  const result = gitHubRepositorySchema.safeParse(value);
  if (!result.success) {
    throw new ConvexError("GitHub returned incomplete repository metadata");
  }
  return {
    defaultBranch: result.data.default_branch,
    ...(result.data.description === null
      ? {}
      : { description: result.data.description }),
    id: result.data.id,
    name: result.data.name,
    owner: result.data.owner.login,
    url: result.data.html_url,
  };
};

export const connectGitHub = action({
  args: { repository: v.string() },
  handler: async (ctx, args): Promise<Id<"projects">> => {
    const ownerId = await requireOwnerId(ctx);
    const { owner, repo } = parseRepository(args.repository);
    const response = await fetch(
      `https://api.github.com/repos/${owner}/${repo}`,
      {
        headers: {
          Accept: "application/vnd.github+json",
          "User-Agent": "zopu-code",
          "X-GitHub-Api-Version": "2022-11-28",
        },
      }
    );
    if (!response.ok) {
      if (response.status === 404) {
        throw new ConvexError(
          "Repository not found. This first flow supports public GitHub repositories."
        );
      }
      throw new ConvexError(`GitHub connection failed with ${response.status}`);
    }
    const repository = readGitHubRepository(await response.json());
    const projectId: Id<"projects"> = await ctx.runMutation(
      internal.projects.storeGitHubProject,
      { ownerId, repository }
    );
    return projectId;
  },
});

export const storeGitHubProject = internalMutation({
  args: {
    ownerId: v.string(),
    repository: v.object({
      defaultBranch: v.string(),
      description: v.optional(v.string()),
      id: v.number(),
      name: v.string(),
      owner: v.string(),
      url: v.string(),
    }),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("projects")
      .withIndex("by_owner_and_repository", (q) =>
        q
          .eq("ownerId", args.ownerId)
          .eq("provider", "github")
          .eq("repoOwner", args.repository.owner)
          .eq("repoName", args.repository.name)
      )
      .unique();
    const timestamp = Date.now();
    if (existing) {
      await ctx.db.patch("projects", existing._id, {
        defaultBranch: args.repository.defaultBranch,
        description: args.repository.description,
        providerRepoId: args.repository.id,
        repoUrl: args.repository.url,
        updatedAt: timestamp,
      });
      return existing._id;
    }

    const projectId = await ctx.db.insert("projects", {
      createdAt: timestamp,
      defaultBranch: args.repository.defaultBranch,
      description: args.repository.description,
      name: args.repository.name,
      ownerId: args.ownerId,
      provider: "github",
      providerRepoId: args.repository.id,
      repoName: args.repository.name,
      repoOwner: args.repository.owner,
      repoUrl: args.repository.url,
      updatedAt: timestamp,
    });
    const artifacts = createInitialArtifacts({
      defaultBranch: args.repository.defaultBranch,
      description: args.repository.description,
      name: args.repository.name,
      repoName: args.repository.name,
      repoOwner: args.repository.owner,
      repoUrl: args.repository.url,
    });
    await Promise.all(
      artifacts.map(({ content, path }) =>
        ctx.db.insert("projectArtifacts", {
          content,
          createdAt: timestamp,
          path,
          projectId,
          revision: 1,
          updatedAt: timestamp,
        })
      )
    );
    await ctx.db.insert("projectSignals", {
      createdAt: timestamp,
      data: { provider: "github", repository: args.repository.url },
      kind: "project.connected",
      projectId,
    });
    return projectId;
  },
});

export const list = query({
  args: {},
  handler: async (ctx) => {
    const ownerId = await requireOwnerId(ctx);
    return await ctx.db
      .query("projects")
      .withIndex("by_owner_and_createdAt", (q) => q.eq("ownerId", ownerId))
      .order("desc")
      .take(50);
  },
});

export const get = query({
  args: { projectId: v.id("projects") },
  handler: async (ctx, args) => {
    const ownerId = await requireOwnerId(ctx);
    const project = await ctx.db.get("projects", args.projectId);
    return project?.ownerId === ownerId ? project : null;
  },
});
