import {
  projectIssueDraftFromSignal,
  queueProjectIssue,
  validateProjectIssueDraft,
  type ProjectIssueDraft,
} from "@code/primitives/project-issue";
import { ConvexError, v } from "convex/values";
import { Effect } from "effect";

import type { Id } from "./_generated/dataModel";
import { mutation, type MutationCtx, query } from "./_generated/server";
import { requireProjectMember } from "./authz";

const toDomainError = (error: unknown) => {
  return new ConvexError(
    error instanceof Error ? error.message : "Invalid project issue"
  );
};

const decodeDraft = async (input: unknown): Promise<ProjectIssueDraft> => {
  try {
    return await Effect.runPromise(validateProjectIssueDraft(input));
  } catch (error) {
    throw toDomainError(error);
  }
};

const draftFromSignal = async (input: unknown): Promise<ProjectIssueDraft> => {
  try {
    return await Effect.runPromise(projectIssueDraftFromSignal(input));
  } catch (error) {
    throw toDomainError(error);
  }
};

const insertIssue = async (
  ctx: MutationCtx,
  projectId: Id<"projects">,
  draft: ProjectIssueDraft
): Promise<Id<"projectIssues">> => {
  const latest = await ctx.db
    .query("projectIssues")
    .withIndex("by_project_and_number", (q) => q.eq("projectId", projectId))
    .order("desc")
    .first();
  const timestamp = Date.now();
  const number = (latest?.number ?? 0) + 1;
  const issueId = await ctx.db.insert("projectIssues", {
    body: draft.body,
    createdAt: timestamp,
    number,
    projectId,
    status: "open",
    title: draft.title,
    updatedAt: timestamp,
  });
  const workArtifact = await ctx.db
    .query("projectArtifacts")
    .withIndex("by_project_and_path", (q) =>
      q.eq("projectId", projectId).eq("path", "work.md")
    )
    .unique();
  if (workArtifact) {
    await ctx.db.patch("projectArtifacts", workArtifact._id, {
      content: `${workArtifact.content}\n## Issue ${number}: ${draft.title}\n\nStatus: open\n\n${draft.body}\n`,
      revision: workArtifact.revision + 1,
      updatedAt: timestamp,
    });
  }
  await ctx.db.insert("projectEvents", {
    createdAt: timestamp,
    data: { number, title: draft.title },
    issueId,
    kind: "issue.created",
    projectId,
  });
  return issueId;
};

export const create = mutation({
  args: {
    body: v.string(),
    projectId: v.id("projects"),
    title: v.string(),
  },
  handler: async (ctx, args) => {
    await requireProjectMember(ctx, args.projectId);
    const draft = await decodeDraft({ body: args.body, title: args.title });
    return insertIssue(ctx, args.projectId, draft);
  },
});

export const createFromSignal = mutation({
  args: { signalId: v.id("signals") },
  handler: async (ctx, args) => {
    const signal = await ctx.db.get("signals", args.signalId);
    if (!signal) {
      throw new ConvexError("Signal not found");
    }
    if (!signal.projectId) {
      throw new ConvexError("Signal is not project-scoped");
    }
    const { organizationId } = await requireProjectMember(
      ctx,
      signal.projectId
    );
    if (signal.organizationId !== organizationId) {
      throw new ConvexError(
        "Signal does not belong to the project organization"
      );
    }
    const draft = await draftFromSignal({
      problemStatement: signal.problemStatement,
      signalId: String(signal._id),
    });
    const issueId = await insertIssue(ctx, signal.projectId, draft);
    return { issueId, projectId: signal.projectId };
  },
});

export const begin = mutation({
  args: { issueId: v.id("projectIssues") },
  handler: async (ctx, args) => {
    const issue = await ctx.db.get("projectIssues", args.issueId);
    if (!issue) {
      throw new ConvexError("Issue not found");
    }
    await requireProjectMember(ctx, issue.projectId);
    let status: "queued" | "working";
    try {
      status = await Effect.runPromise(queueProjectIssue(issue.status));
    } catch (error) {
      throw toDomainError(error);
    }
    if (status === issue.status) {
      return status;
    }
    const timestamp = Date.now();
    await ctx.db.patch("projectIssues", issue._id, {
      status,
      updatedAt: timestamp,
    });
    await ctx.db.insert("projectEvents", {
      createdAt: timestamp,
      data: { number: issue.number },
      issueId: issue._id,
      kind: "issue.queued",
      projectId: issue.projectId,
    });
    return "queued" as const;
  },
});

export const markDispatchFailed = mutation({
  args: { error: v.string(), issueId: v.id("projectIssues") },
  handler: async (ctx, args) => {
    const issue = await ctx.db.get("projectIssues", args.issueId);
    if (!issue) {
      throw new ConvexError("Issue not found");
    }
    await requireProjectMember(ctx, issue.projectId);
    const timestamp = Date.now();
    await ctx.db.patch("projectIssues", issue._id, {
      status: "failed",
      updatedAt: timestamp,
    });
    await ctx.db.insert("projectEvents", {
      createdAt: timestamp,
      data: { error: args.error.slice(0, 1000), phase: "dispatch" },
      issueId: issue._id,
      kind: "agent.failed",
      projectId: issue.projectId,
    });
  },
});

export const list = query({
  args: { projectId: v.id("projects") },
  handler: async (ctx, args) => {
    try {
      await requireProjectMember(ctx, args.projectId);
    } catch {
      return [];
    }
    return await ctx.db
      .query("projectIssues")
      .withIndex("by_project_and_number", (q) =>
        q.eq("projectId", args.projectId)
      )
      .order("desc")
      .take(100);
  },
});

export const events = query({
  args: { projectId: v.id("projects") },
  handler: async (ctx, args) => {
    try {
      await requireProjectMember(ctx, args.projectId);
    } catch {
      return [];
    }
    return await ctx.db
      .query("projectEvents")
      .withIndex("by_project_and_createdAt", (q) =>
        q.eq("projectId", args.projectId)
      )
      .order("desc")
      .take(100);
  },
});
