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

import { internalMutation, mutation, query } from "./_generated/server";
import { requireCurrentOrganization, requireProjectMember } from "./authz";

// Provider ("forge") that owns a repository host. A project's source must be
// served by the same forge whose credentials are attached, so a Gitea token is
// never offered to GitHub and vice versa. Unknown hosts return null, leaving
// the caller free to attach without a forge constraint.
export const forgeForHost = (host: string): "github" | "gitea" | null => {
  const normalized = host.toLowerCase();
  if (normalized === "github.com" || normalized.endsWith(".githost.com")) {
    return "github";
  }
  if (normalized === "git.openputer.com") {
    return "gitea";
  }
  return null;
};

export const persist = internalMutation({
  args: {
    credentialCiphertext: v.string(),
    credentialIv: v.string(),
    credentialKind: v.union(v.literal("oauth"), v.literal("token")),
    provider: v.union(v.literal("github"), v.literal("gitea")),
    serverUrl: v.string(),
    userId: v.string(),
    username: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    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 existing = await ctx.db
      .query("gitConnections")
      .withIndex("by_organizationId_and_provider_and_serverUrl", (q) =>
        q
          .eq("organizationId", organization._id)
          .eq("provider", args.provider)
          .eq("serverUrl", args.serverUrl)
      )
      .unique();
    const timestamp = Date.now();
    if (existing) {
      await ctx.db.patch(existing._id, {
        credentialCiphertext: args.credentialCiphertext,
        credentialIv: args.credentialIv,
        credentialKind: args.credentialKind,
        updatedAt: timestamp,
        username: args.username,
      });
      return existing._id;
    }
    return await ctx.db.insert("gitConnections", {
      connectedAt: timestamp,
      credentialCiphertext: args.credentialCiphertext,
      credentialIv: args.credentialIv,
      credentialKind: args.credentialKind,
      organizationId: organization._id,
      provider: args.provider,
      serverUrl: args.serverUrl,
      updatedAt: timestamp,
      username: args.username,
    });
  },
});

export const list = query({
  args: {},
  handler: async (ctx) => {
    const { organizationId } = await requireCurrentOrganization(ctx);
    const connections = await ctx.db
      .query("gitConnections")
      .withIndex("by_organizationId", (q) =>
        q.eq("organizationId", organizationId)
      )
      .collect();
    return connections.map((connection) => ({
      connectedAt: connection.connectedAt,
      credentialKind: connection.credentialKind,
      id: String(connection._id),
      provider: connection.provider,
      serverUrl: connection.serverUrl,
      username: connection.username,
    }));
  },
});

export const getForProject = query({
  args: { projectId: v.id("projects") },
  handler: async (ctx, args) => {
    await requireProjectMember(ctx, args.projectId);
    const project = await ctx.db.get(args.projectId);
    const connection = project?.gitConnectionId
      ? await ctx.db.get(project.gitConnectionId)
      : null;
    return connection
      ? {
          connectedAt: connection.connectedAt,
          credentialKind: connection.credentialKind,
          id: String(connection._id),
          provider: connection.provider,
          serverUrl: connection.serverUrl,
          username: connection.username,
        }
      : null;
  },
});

export const attachToProject = mutation({
  args: {
    connectionId: v.id("gitConnections"),
    projectId: v.id("projects"),
  },
  handler: async (ctx, args) => {
    const { organizationId } = await requireProjectMember(ctx, args.projectId);
    const connection = await ctx.db.get(args.connectionId);
    if (!connection || connection.organizationId !== organizationId) {
      throw new ConvexError("Git connection not found");
    }
    const project = await ctx.db.get(args.projectId);
    if (project) {
      const expected = forgeForHost(project.sourceHost);
      if (expected && connection.provider !== expected) {
        throw new ConvexError(
          `Git credential provider (${connection.provider}) does not match this project's forge (${expected})`
        );
      }
    }
    await ctx.db.patch(args.projectId, {
      gitConnectionId: connection._id,
      updatedAt: Date.now(),
    });
    return { attached: true };
  },
});
