import type { GraphQLContext } from "../../context.js";
import { GraphQLError } from "graphql";
import {
  db,
  and,
  eq,
  sql,
  agents,
  tenants,
  threads,
  messages,
  messageMentions,
  spaces,
  spaceAgentAssignments,
  threadParticipants,
  threadToCamel,
} from "../../utils.js";
import { notifyThreadUpdate } from "../../notify.js";
import { requireTenantMember } from "../core/authz.js";
import { resolveCallerFromAuth } from "../core/resolve-auth-user.js";
import {
  enqueueComputerThreadTurn,
  resolveThreadComputer,
  routeRunbookForComputerMessage,
} from "../../../lib/computers/thread-cutover.js";
import { ensureDefaultThreadSpace } from "../../../lib/spaces/default-space.js";
import { dispatchAgentMentions } from "../../../lib/mentions/dispatch-agent-mentions.js";
import { dispatchDefaultAgentTurn } from "../../../lib/mentions/default-agent-routing.js";
import { parseMessageMentions } from "../../../lib/mentions/parse-message-mentions.js";
import {
  insertMentionParticipants,
  toThreadParticipantInsert,
} from "../../../lib/mentions/thread-participant-mentions.js";
import { loadThreadMentionTargets } from "../../../lib/mentions/thread-mention-targets.js";
import { canPostToSpace } from "../spaces/shared.js";

export const createThread = async (
  _parent: any,
  args: any,
  ctx: GraphQLContext,
) => {
  const i = args.input;

  // Tenant gate. Cognito callers must belong to the target tenant — without
  // this, an authenticated user holding a JWT for tenant A could supply
  // i.tenantId = tenant B and create threads in B's namespace. Closes the
  // F5 P0 from #959 review. apikey callers are pre-authorized service
  // identities (agentcore runtime, schedulers); they are validated by the
  // shared API secret and may legitimately create threads across tenants.
  if (ctx.auth.authType === "cognito") {
    await requireTenantMember(ctx, i.tenantId);
  }

  const createdByType = i.createdByType ?? "user";
  const createdById =
    createdByType === "user"
      ? ((await resolveCallerFromAuth(ctx.auth)).userId ?? i.createdById)
      : i.createdById;
  if (createdByType === "user" && !createdById) {
    throw new GraphQLError("Requester user identity required", {
      extensions: { code: "UNAUTHENTICATED" },
    });
  }

  let threadSpace: { id: string; tenant_id: string; status: string };
  if (!i.spaceId) {
    threadSpace = await ensureDefaultThreadSpace({
      tenantId: i.tenantId,
      userId: createdByType === "user" ? createdById : null,
    });
  } else {
    const [spaceRow] = await db
      .select({
        id: spaces.id,
        tenant_id: spaces.tenant_id,
        status: spaces.status,
      })
      .from(spaces)
      .where(eq(spaces.id, i.spaceId));
    if (
      !spaceRow ||
      spaceRow.tenant_id !== i.tenantId ||
      spaceRow.status !== "active"
    ) {
      throw new GraphQLError("Space not found", {
        extensions: { code: "NOT_FOUND" },
      });
    }
    threadSpace = spaceRow;
  }
  if (
    ctx.auth.authType === "cognito" &&
    !(await canPostToSpace(ctx, i.tenantId, threadSpace.id))
  ) {
    throw new GraphQLError("Space access required", {
      extensions: { code: "FORBIDDEN" },
    });
  }
  const threadComputer = await resolveThreadComputer({
    tenantId: i.tenantId,
    ownerUserId: createdByType === "user" ? createdById : null,
    requesterUserId: createdByType === "user" ? createdById : null,
    requestedComputerId: i.computerId ?? null,
  });

  // PRD-09 §9.4.4: Agent-created thread validation
  if (createdByType === "agent" && createdById) {
    const [creatorAgent] = await db
      .select({ tenant_id: agents.tenant_id })
      .from(agents)
      .where(eq(agents.id, createdById));
    if (!creatorAgent || creatorAgent.tenant_id !== i.tenantId) {
      throw new Error("Agent can only create threads in its own tenant");
    }
  }

  const channel = (i.channel?.toLowerCase() ?? "manual") as string;
  const CHANNEL_PREFIX: Record<string, string> = {
    schedule: "AUTO",
    email: "EMAIL",
    chat: "CHAT",
    manual: "TICK",
    webhook: "HOOK",
    api: "API",
  };
  const prefix = CHANNEL_PREFIX[channel] || "TICK";
  const initialStatus =
    channel === "chat" || channel === "schedule" ? "in_progress" : "backlog";

  // Atomic: counter bump + thread insert + optional first user message. Keeps
  // hosts from needing a two-round-trip create-then-send and prevents orphan
  // threads if the message insert fails.
  const openingMessageCreatedAt = i.firstMessage ? new Date() : null;
  const { row, firstMessageId } = await db.transaction(async (tx) => {
    const [tenant] = await tx
      .update(tenants)
      .set({
        issue_counter: sql`${tenants.issue_counter} + 1`,
      })
      .where(eq(tenants.id, i.tenantId))
      .returning({
        next_number: sql<number>`${tenants.issue_counter}`,
      });
    if (!tenant) throw new Error("Tenant not found");
    const nextNumber = tenant.next_number;
    const identifier = `${prefix}-${nextNumber}`;

    // Mirror sendMessage's auto-title logic so the atomic path matches the
    // two-step flow when callers leave title as "Untitled conversation".
    let effectiveTitle = i.title;
    if (i.firstMessage && i.title === "Untitled conversation") {
      const raw = i.firstMessage.trim();
      effectiveTitle =
        raw.length <= 80 ? raw : raw.substring(0, 80).replace(/\s+\S*$/, "...");
    }

    const [threadRow] = await tx
      .insert(threads)
      .values({
        tenant_id: i.tenantId,
        agent_id: threadComputer ? null : i.agentId,
        computer_id: threadComputer?.id,
        space_id: threadSpace.id,
        user_id: createdByType === "user" ? createdById : undefined,
        number: nextNumber,
        identifier,
        title: effectiveTitle,
        status: initialStatus,
        channel,
        assignee_type: i.assigneeType,
        assignee_id: i.assigneeId,
        billing_code: i.billingCode,
        created_by_type: createdByType,
        created_by_id: createdById,
        labels: i.labels ? JSON.parse(i.labels) : undefined,
        metadata: i.metadata ? JSON.parse(i.metadata) : undefined,
        due_at: i.dueAt ? new Date(i.dueAt) : undefined,
        created_at: openingMessageCreatedAt ?? undefined,
        updated_at: openingMessageCreatedAt ?? undefined,
      })
      .returning();

    const participantRows: (typeof threadParticipants.$inferInsert)[] = [];
    if (createdByType === "user" && createdById) {
      participantRows.push({
        tenant_id: i.tenantId,
        thread_id: threadRow.id,
        space_id: threadSpace.id,
        participant_type: "user",
        user_id: createdById,
        role: "requester",
        source: "thread_creator",
        last_read_at: openingMessageCreatedAt ?? undefined,
      });
    }

    const autoSubscribedAgents = await tx
      .select({
        agent_id: spaceAgentAssignments.agent_id,
        local_role: spaceAgentAssignments.local_role,
      })
      .from(spaceAgentAssignments)
      .where(
        and(
          eq(spaceAgentAssignments.tenant_id, i.tenantId),
          eq(spaceAgentAssignments.space_id, threadSpace.id),
          eq(spaceAgentAssignments.status, "active"),
          eq(spaceAgentAssignments.auto_subscribe, true),
        ),
      );
    for (const assignment of autoSubscribedAgents) {
      participantRows.push({
        tenant_id: i.tenantId,
        thread_id: threadRow.id,
        space_id: threadSpace.id,
        participant_type: "agent",
        agent_id: assignment.agent_id,
        role: assignment.local_role ?? "agent",
        source: "space_auto_subscribe",
        notification_preference: "subscribed",
      });
    }

    if (participantRows.length > 0) {
      await tx.insert(threadParticipants).values(participantRows);
    }

    let firstMsgId: string | null = null;
    if (i.firstMessage) {
      const [msgRow] = await tx
        .insert(messages)
        .values({
          thread_id: threadRow.id,
          tenant_id: i.tenantId,
          role: "user",
          content: i.firstMessage,
          sender_type: createdByType,
          sender_id: createdById,
          created_at: openingMessageCreatedAt ?? undefined,
        })
        .returning({ id: messages.id });
      firstMsgId = msgRow?.id ?? null;
    }

    return { row: threadRow, firstMessageId: firstMsgId };
  });

  // Side effects after commit (non-transactional — Lambda invoke + SNS notify).
  notifyThreadUpdate({
    threadId: row.id,
    tenantId: row.tenant_id,
    status: row.status,
    title: row.title,
  }).catch(() => {});

  const parsedOpeningMentions =
    firstMessageId && i.firstMessage
      ? await persistOpeningMessageMentions({
          tenantId: row.tenant_id,
          threadId: row.id,
          spaceId: row.space_id,
          messageId: firstMessageId,
          content: i.firstMessage,
        })
      : [];

  const hasOpeningAgentMentions = parsedOpeningMentions.some(
    (mention) => mention.targetType === "agent",
  );
  if (hasOpeningAgentMentions && firstMessageId) {
    try {
      await dispatchAgentMentions({
        tenantId: row.tenant_id,
        threadId: row.id,
        spaceId: row.space_id,
        messageId: firstMessageId,
        content: i.firstMessage,
        mentions: parsedOpeningMentions,
        sender: { type: createdByType, id: createdById },
      });
    } catch (err) {
      console.warn("[createThread] agent mention dispatch failed:", err);
    }
  }

  if (
    firstMessageId &&
    i.firstMessage &&
    createdByType === "user" &&
    parsedOpeningMentions.length === 0 &&
    !row.computer_id
  ) {
    try {
      await dispatchDefaultAgentTurn({
        tenantId: row.tenant_id,
        threadId: row.id,
        spaceId: row.space_id,
        messageId: firstMessageId,
        content: i.firstMessage,
        sender: { type: createdByType, id: createdById },
      });
    } catch (err) {
      console.warn("[createThread] default agent dispatch failed:", err);
    }
  }

  if (firstMessageId && row.computer_id && parsedOpeningMentions.length === 0) {
    const handledByRunbook = await routeRunbookForComputerMessage({
      tenantId: row.tenant_id,
      computerId: row.computer_id,
      threadId: row.id,
      messageId: firstMessageId,
      prompt: i.firstMessage ?? "",
      actorType: createdByType,
      actorId: createdById,
    });
    if (handledByRunbook) {
      return threadToCamel(row);
    }
    await enqueueComputerThreadTurn({
      tenantId: row.tenant_id,
      computerId: row.computer_id,
      threadId: row.id,
      messageId: firstMessageId,
      source: "chat_message",
      actorType: createdByType,
      actorId: createdById,
    });
    return threadToCamel(row);
  }

  return threadToCamel(row);
};

async function persistOpeningMessageMentions(input: {
  tenantId: string;
  threadId: string;
  spaceId: string | null;
  messageId: string;
  content: string;
}) {
  const mentionTargets = await loadThreadMentionTargets({
    tenantId: input.tenantId,
    threadId: input.threadId,
  });
  const parsedMentions = parseMessageMentions({
    content: input.content,
    targets: mentionTargets,
  });
  if (parsedMentions.length === 0) return parsedMentions;

  await db.transaction(async (tx) => {
    await tx
      .insert(messageMentions)
      .values(
        parsedMentions.map((mention) => ({
          tenant_id: input.tenantId,
          thread_id: input.threadId,
          message_id: input.messageId,
          target_type: mention.targetType,
          target_id: mention.targetId,
          display_name: mention.displayName,
          raw_text: mention.rawText,
          start_offset: mention.startOffset,
          end_offset: mention.endOffset,
        })),
      )
      .onConflictDoNothing();

    await insertMentionParticipants(
      {
        tenantId: input.tenantId,
        threadId: input.threadId,
        spaceId: input.spaceId,
        mentions: parsedMentions,
        targets: mentionTargets,
      },
      {
        async insertParticipants(rows) {
          await tx
            .insert(threadParticipants)
            .values(rows.map(toThreadParticipantInsert))
            .onConflictDoNothing();
        },
      },
    );
  });

  return parsedMentions;
}
