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

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

// ---------------------------------------------------------------------------
// Agent token guard (service-level auth for Zopu tools).
// ---------------------------------------------------------------------------

const requireAgent = (token: string) => {
  if (token !== env.FLUE_DB_TOKEN) {
    throw new ConvexError("Invalid agent control token");
  }
};

const PROBLEM_STATEMENT = v.object({
  title: v.string(),
  summary: v.string(),
  desiredOutcome: v.string(),
  constraints: v.array(v.string()),
});

// ---------------------------------------------------------------------------
// View helpers
// ---------------------------------------------------------------------------

interface EvidenceMessage {
  readonly messageId: string;
  readonly rawText: string;
  readonly createdAt: number;
  readonly submissionId: string | null;
}

interface ActiveIssueView {
  readonly _id: Id<"projectIssues">;
  readonly number: number;
  readonly title: string;
  readonly body: string;
  readonly status: string;
  readonly projectId: Id<"projects">;
  readonly updatedAt: number;
}

interface ProjectSummaryView {
  readonly _id: Id<"projects">;
  readonly name: string;
  readonly description: string | null;
  readonly organizationId: Id<"organizations">;
}

interface ContextDocumentView {
  readonly kind: string;
  readonly path: string;
  readonly content: string;
  readonly revision: number;
}

interface SignalSummaryView {
  readonly _id: Id<"signals">;
  readonly problemStatement: {
    readonly title: string;
    readonly summary: string;
    readonly desiredOutcome: string;
    readonly constraints: readonly string[];
  };
  readonly projectId: Id<"projects"> | null;
  readonly createdAt: number;
}

// ---------------------------------------------------------------------------
// Organization + project authorization (agent-gated, no user identity).
// The agent instance id IS the organization id; the tool never supplies
// tenancy. We verify the organization exists and the project belongs to it.
// ---------------------------------------------------------------------------

const authorizeProjectInOrg = async (
  ctx: MutationCtx | QueryCtx,
  organizationId: Id<"organizations">,
  projectId: Id<"projects">
): Promise<Doc<"projects">> => {
  const project = await ctx.db.get(projectId);
  if (!project) {
    throw new ConvexError("Project not found");
  }
  if (project.organizationId !== organizationId) {
    throw new ConvexError("Project does not belong to the target organization");
  }
  return project;
};

const authorizeSignalInOrg = async (
  ctx: MutationCtx | QueryCtx,
  organizationId: Id<"organizations">,
  signalId: Id<"signals">
): Promise<Doc<"signals">> => {
  const signal = await ctx.db.get(signalId);
  if (!signal) {
    throw new ConvexError("Signal not found");
  }
  if (signal.organizationId !== organizationId) {
    throw new ConvexError("Signal does not belong to the target organization");
  }
  return signal;
};

// ---------------------------------------------------------------------------
// 1. List admitted user messages not yet consumed by a Signal.
// ---------------------------------------------------------------------------

export const listEvidence = query({
  args: {
    organizationId: v.id("organizations"),
    token: v.string(),
  },
  handler: async (ctx, args): Promise<EvidenceMessage[]> => {
    requireAgent(args.token);
    const conversationId = args.organizationId;

    const admitted = await ctx.db
      .query("conversationMessages")
      .withIndex("by_organization_and_message", (q) =>
        q
          .eq("organizationId", args.organizationId)
          .eq("conversationId", conversationId)
      )
      .order("desc")
      .take(100);

    const candidates = admitted.filter(
      (message) => message.role === "user" && message.status === "admitted"
    );

    const unused: EvidenceMessage[] = [];
    for (const message of candidates) {
      const consumed = await ctx.db
        .query("signalSources")
        .withIndex("by_organization_and_message", (q) =>
          q
            .eq("organizationId", args.organizationId)
            .eq("conversationId", conversationId)
            .eq("messageId", message.messageId)
        )
        .first();
      if (!consumed) {
        unused.push({
          createdAt: message.createdAt,
          messageId: message.messageId,
          rawText: message.rawText,
          submissionId: message.submissionId ?? null,
        });
      }
    }

    return unused;
  },
});

// ---------------------------------------------------------------------------
// 2. Create a Signal from selected messages + structured problem statement.
// ---------------------------------------------------------------------------

const buildSourceKey = (
  organizationId: Id<"organizations">,
  conversationId: string,
  messageIds: readonly string[]
): string => JSON.stringify([organizationId, conversationId, ...messageIds]);

const resolveSources = async (
  ctx: MutationCtx,
  organizationId: Id<"organizations">,
  conversationId: string,
  messageIds: readonly string[]
): Promise<Doc<"conversationMessages">[]> => {
  if (messageIds.length === 0) {
    throw new ConvexError("At least one source message is required");
  }
  if (new Set(messageIds).size !== messageIds.length) {
    throw new ConvexError("Source message ids must be unique");
  }
  const resolved: Doc<"conversationMessages">[] = [];
  for (const messageId of messageIds) {
    const doc = await ctx.db
      .query("conversationMessages")
      .withIndex("by_organization_and_message", (q) =>
        q
          .eq("organizationId", organizationId)
          .eq("conversationId", conversationId)
          .eq("messageId", messageId)
      )
      .unique();
    if (!doc) {
      throw new ConvexError(`Source message not found: ${messageId}`);
    }
    if (doc.role !== "user") {
      throw new ConvexError(
        `Source message is not a user message: ${messageId}`
      );
    }
    if (doc.status !== "admitted") {
      throw new ConvexError(`Source message is not admitted: ${messageId}`);
    }
    resolved.push(doc);
  }
  return resolved;
};

export const createSignal = mutation({
  args: {
    organizationId: v.id("organizations"),
    projectId: v.optional(v.id("projects")),
    messageIds: v.array(v.string()),
    problemStatement: PROBLEM_STATEMENT,
    processedByAgentInstanceId: v.string(),
    token: v.string(),
  },
  handler: async (ctx, args): Promise<{ signalId: Id<"signals"> }> => {
    requireAgent(args.token);
    if (args.projectId) {
      await authorizeProjectInOrg(ctx, args.organizationId, args.projectId);
    }

    const conversationId = args.organizationId;
    if (conversationId !== args.organizationId) {
      throw new ConvexError("Conversation does not belong to organization");
    }

    const sourceKey = buildSourceKey(
      args.organizationId,
      conversationId,
      args.messageIds
    );

    const existing = await ctx.db
      .query("signals")
      .withIndex("by_organization_and_sourceKey", (q) =>
        q.eq("organizationId", args.organizationId).eq("sourceKey", sourceKey)
      )
      .unique();
    if (existing) {
      return { signalId: existing._id };
    }

    const sources = await resolveSources(
      ctx,
      args.organizationId,
      conversationId,
      args.messageIds
    );

    const now = Date.now();
    const prospectiveId = crypto.randomUUID();
    const scope =
      args.projectId === undefined
        ? { _tag: "Organization" as const, organizationId: args.organizationId }
        : {
            _tag: "Project" as const,
            organizationId: args.organizationId,
            projectId: args.projectId,
          };

    const signal = await Effect.runPromise(
      composeSignal({
        createdAt: now,
        id: prospectiveId,
        problemStatement: args.problemStatement,
        processedBy: {
          agentInstanceId: args.processedByAgentInstanceId,
          agentName: "zopu",
        },
        scope,
        sourceMessages: sources.map((doc) => ({
          conversationId: doc.conversationId,
          createdAt: doc.createdAt,
          messageId: doc.messageId,
          rawText: doc.rawText,
        })),
      })
    ).catch((error: unknown) => {
      if (error instanceof SignalValidationError) {
        throw new ConvexError(`Invalid signal: ${error.message}`);
      }
      throw new ConvexError(
        `Invalid signal: ${error instanceof Error ? error.message : "unknown"}`
      );
    });

    const signalId = await ctx.db.insert("signals", {
      conversationId,
      createdAt: now,
      organizationId: args.organizationId,
      problemStatement: {
        constraints: [...signal.problemStatement.constraints],
        desiredOutcome: signal.problemStatement.desiredOutcome,
        summary: signal.problemStatement.summary,
        title: signal.problemStatement.title,
      },
      processedByAgentInstanceId: args.processedByAgentInstanceId,
      processedByAgentName: "zopu",
      ...(args.projectId === undefined ? {} : { projectId: args.projectId }),
      sourceKey,
    });

    await Promise.all(
      sources.map((doc, ordinal) =>
        ctx.db.insert("signalSources", {
          conversationId: doc.conversationId,
          messageId: doc.messageId,
          ordinal,
          organizationId: args.organizationId,
          rawTextSnapshot: doc.rawText,
          signalId,
          ...(doc.submissionId === undefined
            ? {}
            : { submissionId: doc.submissionId }),
          sourceCreatedAt: doc.createdAt,
        })
      )
    );

    return { signalId };
  },
});

// ---------------------------------------------------------------------------
// 3. List recent Signals for a project (or organization-wide when no project).
// ---------------------------------------------------------------------------

export const listSignals = query({
  args: {
    organizationId: v.id("organizations"),
    projectId: v.optional(v.id("projects")),
    token: v.string(),
  },
  handler: async (ctx, args): Promise<SignalSummaryView[]> => {
    requireAgent(args.token);
    if (args.projectId) {
      await authorizeProjectInOrg(ctx, args.organizationId, args.projectId);
      const signals = await ctx.db
        .query("signals")
        .withIndex("by_project_and_createdAt", (q) =>
          q.eq("projectId", args.projectId as Id<"projects">)
        )
        .order("desc")
        .take(20);
      return signals.map((s) => ({
        _id: s._id,
        createdAt: s.createdAt,
        problemStatement: s.problemStatement,
        projectId: s.projectId ?? null,
      }));
    }

    const signals = await ctx.db
      .query("signals")
      .withIndex("by_organization_and_createdAt", (q) =>
        q.eq("organizationId", args.organizationId)
      )
      .order("desc")
      .take(20);
    return signals.map((s) => ({
      _id: s._id,
      createdAt: s.createdAt,
      problemStatement: s.problemStatement,
      projectId: s.projectId ?? null,
    }));
  },
});

// ---------------------------------------------------------------------------
// 4. List active (open/queued/working/needs-input) ProjectIssues for a project.
// ---------------------------------------------------------------------------

export const listActiveIssues = query({
  args: {
    organizationId: v.id("organizations"),
    projectId: v.id("projects"),
    token: v.string(),
  },
  handler: async (ctx, args): Promise<ActiveIssueView[]> => {
    requireAgent(args.token);
    await authorizeProjectInOrg(ctx, args.organizationId, args.projectId);

    const issues = await ctx.db
      .query("projectIssues")
      .withIndex("by_project_and_status", (q) =>
        q.eq("projectId", args.projectId)
      )
      .take(100);

    return issues
      .filter(
        (issue) =>
          issue.status === "open" ||
          issue.status === "queued" ||
          issue.status === "working" ||
          issue.status === "needs-input"
      )
      .map((issue) => ({
        _id: issue._id,
        body: issue.body,
        number: issue.number,
        projectId: issue.projectId,
        status: issue.status,
        title: issue.title,
        updatedAt: issue.updatedAt,
      }))
      .sort((a, b) => b.updatedAt - a.updatedAt);
  },
});

// ---------------------------------------------------------------------------
// 5. Attach a Signal to an existing ProjectIssue (idempotent).
// ---------------------------------------------------------------------------

export const attachSignalToIssue = mutation({
  args: {
    organizationId: v.id("organizations"),
    signalId: v.id("signals"),
    issueId: v.id("projectIssues"),
    token: v.string(),
  },
  handler: async (
    ctx,
    args
  ): Promise<{
    attachmentId: Id<"signalIssueAttachments">;
    alreadyAttached: boolean;
  }> => {
    requireAgent(args.token);
    const signal = await authorizeSignalInOrg(
      ctx,
      args.organizationId,
      args.signalId
    );
    const issue = await ctx.db.get(args.issueId);
    if (!issue) {
      throw new ConvexError("Issue not found");
    }
    await authorizeProjectInOrg(ctx, args.organizationId, issue.projectId);

    // Cross-project rejection: the signal must be project-scoped and its
    // project must match the issue's project. Organization equality alone is
    // insufficient — it would permit attaching across different projects in
    // the same org.
    if (!signal.projectId) {
      throw new ConvexError("Signal is not project-scoped");
    }
    if (signal.projectId !== issue.projectId) {
      throw new ConvexError("Signal and issue must belong to the same project");
    }

    // Idempotent: if the attachment already exists, return it without
    // emitting a duplicate event.
    const existing = await ctx.db
      .query("signalIssueAttachments")
      .withIndex("by_signal_and_issue", (q) =>
        q.eq("signalId", args.signalId).eq("issueId", args.issueId)
      )
      .unique();
    if (existing) {
      return {
        alreadyAttached: true,
        attachmentId: existing._id,
      };
    }

    const now = Date.now();
    const attachmentId = await ctx.db.insert("signalIssueAttachments", {
      createdAt: now,
      issueId: args.issueId,
      organizationId: args.organizationId,
      projectId: issue.projectId,
      signalId: args.signalId,
    });

    await ctx.db.insert("projectEvents", {
      createdAt: now,
      data: {
        signalProblemTitle: signal.problemStatement.title,
        signalId: String(signal._id),
      },
      issueId: args.issueId,
      kind: "signal.attached",
      projectId: issue.projectId,
    });

    return { alreadyAttached: false, attachmentId };
  },
});

// ---------------------------------------------------------------------------
// 6. Create a new ProjectIssue from a Signal.
// ---------------------------------------------------------------------------

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,
  });
  await ctx.db.insert("projectEvents", {
    createdAt: timestamp,
    data: { number, source: "signal", title: draft.title },
    issueId,
    kind: "issue.created",
    projectId,
  });
  return issueId;
};

export const createIssueFromSignal = mutation({
  args: {
    organizationId: v.id("organizations"),
    signalId: v.id("signals"),
    token: v.string(),
  },
  handler: async (
    ctx,
    args
  ): Promise<{
    created: boolean;
    issueId: Id<"projectIssues">;
    projectId: Id<"projects">;
  }> => {
    requireAgent(args.token);
    const signal = await authorizeSignalInOrg(
      ctx,
      args.organizationId,
      args.signalId
    );
    if (!signal.projectId) {
      throw new ConvexError("Signal is not project-scoped");
    }
    await authorizeProjectInOrg(ctx, args.organizationId, signal.projectId);

    // Idempotent: if any attachment already exists for this signal, return
    // the existing issue without creating a duplicate. This prevents Flue
    // retries from producing duplicate issues/attachments.
    const existingAttachment = await ctx.db
      .query("signalIssueAttachments")
      .withIndex("by_signal", (q) => q.eq("signalId", args.signalId))
      .first();
    if (existingAttachment) {
      return {
        created: false,
        issueId: existingAttachment.issueId,
        projectId: existingAttachment.projectId,
      };
    }

    const draft = await Effect.runPromise(
      projectIssueDraftFromSignal({
        problemStatement: signal.problemStatement,
        signalId: String(signal._id),
      })
    ).catch((error: unknown) => {
      throw new ConvexError(
        error instanceof Error ? error.message : "Invalid project issue"
      );
    });

    const issueId = await insertIssue(ctx, signal.projectId, draft);

    // Auto-attach the signal to the new issue.
    const now = Date.now();
    await ctx.db.insert("signalIssueAttachments", {
      createdAt: now,
      issueId,
      organizationId: args.organizationId,
      projectId: signal.projectId,
      signalId: signal._id,
    });

    return { created: true, issueId, projectId: signal.projectId };
  },
});

// ---------------------------------------------------------------------------
// 7. Begin a ProjectIssue (transition to queued/working).
// ---------------------------------------------------------------------------

export const beginIssue = mutation({
  args: {
    organizationId: v.id("organizations"),
    issueId: v.id("projectIssues"),
    token: v.string(),
  },
  handler: async (ctx, args): Promise<"queued" | "working"> => {
    requireAgent(args.token);
    const issue = await ctx.db.get(args.issueId);
    if (!issue) {
      throw new ConvexError("Issue not found");
    }
    await authorizeProjectInOrg(ctx, args.organizationId, issue.projectId);

    const status = await Effect.runPromise(
      queueProjectIssue(issue.status)
    ).catch((error: unknown) => {
      throw new ConvexError(
        error instanceof Error ? error.message : "Invalid issue status"
      );
    });
    if (status === issue.status) {
      return status;
    }

    const timestamp = Date.now();
    await ctx.db.patch(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 status;
  },
});

// ---------------------------------------------------------------------------
// 8. Get project summary + context documents for routing decisions.
// ---------------------------------------------------------------------------

export const getProjectContext = query({
  args: {
    organizationId: v.id("organizations"),
    projectId: v.id("projects"),
    token: v.string(),
  },
  handler: async (
    ctx,
    args
  ): Promise<{
    project: ProjectSummaryView;
    contextDocuments: ContextDocumentView[];
  }> => {
    requireAgent(args.token);
    const project = await authorizeProjectInOrg(
      ctx,
      args.organizationId,
      args.projectId
    );

    const docs = await ctx.db
      .query("projectContextDocuments")
      .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
      .take(25);

    return {
      contextDocuments: docs.map((doc) => ({
        content: doc.content,
        kind: doc.kind,
        path: doc.path,
        revision: doc.revision,
      })),
      project: {
        _id: project._id,
        description: project.description ?? null,
        name: project.name,
        organizationId: project.organizationId,
      },
    };
  },
});

// ---------------------------------------------------------------------------
// 9. List projects in the organization (for selecting a project context).
// ---------------------------------------------------------------------------

export const listProjects = query({
  args: {
    organizationId: v.id("organizations"),
    token: v.string(),
  },
  handler: async (ctx, args): Promise<ProjectSummaryView[]> => {
    requireAgent(args.token);
    const projects = await ctx.db
      .query("projects")
      .withIndex("by_organization_and_createdAt", (q) =>
        q.eq("organizationId", args.organizationId)
      )
      .take(50);
    return projects.map((p) => ({
      _id: p._id,
      description: p.description ?? null,
      name: p.name,
      organizationId: p.organizationId,
    }));
  },
});
