import {
  decodePublicGitImportResult,
  preparePublicGitSource,
} from "@code/primitives/project";
import type {
  ProjectImportOutcome,
  ProjectView,
} from "@code/primitives/project";
import { ConvexError, v } from "convex/values";
import { Effect } from "effect";

import { internal } from "./_generated/api";
import type { Doc, Id } from "./_generated/dataModel";
import { action, internalMutation, query } from "./_generated/server";
import { requireAuthUserId, requireCurrentOrganization } from "./authz";
import { inspectPublicGit } from "./publicGit";

const toProjectView = async (
  ctx: Parameters<typeof requireCurrentOrganization>[0],
  project: Doc<"projects">
): Promise<ProjectView> => {
  const documents = await ctx.db
    .query("projectContextDocuments")
    .withIndex("by_projectId_and_path", (q) => q.eq("projectId", project._id))
    .collect();
  const view = {
    contextDocuments: documents.map((document) => ({
      content: document.content,
      kind: document.kind,
      origin: document.origin,
      path: document.path,
      revision: 1,
      sourceUrl: document.sourceUrl,
    })),
    createdAt: project.createdAt,
    id: String(project._id),
    name: project.name,
    organizationId: String(project.organizationId),
    sources: [
      {
        createdAt: project.createdAt,
        defaultBranch: project.defaultBranch,
        host: project.sourceHost,
        kind: "git",
        normalizedUrl: project.normalizedSourceUrl,
        projectId: String(project._id),
        repositoryPath: project.repositoryPath,
        updatedAt: project.updatedAt,
        url: project.sourceUrl,
      },
    ],
    updatedAt: project.updatedAt,
  };
  return view as unknown as ProjectView;
};

export const persistPublicGitImport = internalMutation({
  args: {
    remote: v.object({
      defaultBranch: v.optional(v.string()),
      documents: v.array(
        v.object({
          content: v.string(),
          kind: v.union(
            v.literal("readme"),
            v.literal("agents"),
            v.literal("product"),
            v.literal("business"),
            v.literal("design"),
            v.literal("tech")
          ),
          path: v.string(),
        })
      ),
      warnings: v.array(v.object({ message: v.string(), path: v.string() })),
    }),
    source: v.object({
      host: v.string(),
      normalizedUrl: v.string(),
      projectName: v.string(),
      repositoryPath: v.string(),
      url: v.string(),
    }),
    userId: v.string(),
  },
  handler: async (ctx, args): Promise<ProjectImportOutcome> => {
    const organization = await ctx.db
      .query("organizations")
      .withIndex("by_createdBy_and_kind", (q) =>
        q.eq("createdBy", args.userId).eq("kind", "personal")
      )
      .unique();
    if (!organization) {
      throw new ConvexError("Organization not found");
    }
    const timestamp = Date.now();
    const existing = await ctx.db
      .query("projects")
      .withIndex("by_organizationId_and_normalizedSourceUrl", (q) =>
        q
          .eq("organizationId", organization._id)
          .eq("normalizedSourceUrl", args.source.normalizedUrl)
      )
      .unique();
    let projectId: Id<"projects">;
    if (existing) {
      projectId = existing._id;
      await ctx.db.patch(projectId, {
        defaultBranch: args.remote.defaultBranch,
        name: args.source.projectName,
        repositoryPath: args.source.repositoryPath,
        sourceHost: args.source.host,
        sourceUrl: args.source.url,
        updatedAt: timestamp,
      });
      const oldDocuments = await ctx.db
        .query("projectContextDocuments")
        .withIndex("by_projectId_and_path", (q) => q.eq("projectId", projectId))
        .collect();
      for (const document of oldDocuments) {
        await ctx.db.delete(document._id);
      }
    } else {
      projectId = await ctx.db.insert("projects", {
        createdAt: timestamp,
        defaultBranch: args.remote.defaultBranch,
        name: args.source.projectName,
        normalizedSourceUrl: args.source.normalizedUrl,
        organizationId: organization._id,
        repositoryPath: args.source.repositoryPath,
        sourceHost: args.source.host,
        sourceUrl: args.source.url,
        updatedAt: timestamp,
      });
    }
    for (const document of args.remote.documents) {
      await ctx.db.insert("projectContextDocuments", {
        ...document,
        createdAt: timestamp,
        origin: "repository",
        projectId,
        sourceUrl: args.source.url,
        updatedAt: timestamp,
      });
    }
    const project = await ctx.db.get(projectId);
    if (!project) {
      throw new Error("Project could not be read after import");
    }
    const view = await toProjectView(ctx, project);
    return { ...view, source: view.sources[0]! };
  },
});

export const list = query({
  args: {},
  handler: async (ctx): Promise<ProjectView[]> => {
    const { organizationId } = await requireCurrentOrganization(ctx);
    const projects = await ctx.db
      .query("projects")
      .withIndex("by_organizationId_and_createdAt", (q) =>
        q.eq("organizationId", organizationId)
      )
      .order("desc")
      .take(50);
    return await Promise.all(
      projects.map((project) => toProjectView(ctx, project))
    );
  },
});

export const get = query({
  args: { projectId: v.id("projects") },
  handler: async (ctx, args): Promise<ProjectView | null> => {
    const { organizationId } = await requireCurrentOrganization(ctx);
    const project = await ctx.db.get(args.projectId);
    return project?.organizationId === organizationId
      ? await toProjectView(ctx, project)
      : null;
  },
});

export const importPublicGit = action({
  args: { repositoryUrl: v.string() },
  handler: async (ctx, args): Promise<ProjectImportOutcome> => {
    const userId = await requireAuthUserId(ctx);
    const source = await Effect.runPromise(
      preparePublicGitSource(args.repositoryUrl)
    );
    const remote = await Effect.runPromise(
      decodePublicGitImportResult(await inspectPublicGit(source))
    );
    return await ctx.runMutation(internal.projects.persistPublicGitImport, {
      remote: {
        defaultBranch: remote.defaultBranch,
        documents: remote.documents.map((document) => ({ ...document })),
        warnings: remote.warnings.map((warning) => ({ ...warning })),
      },
      source,
      userId,
    });
  },
});
