import { buildSchema } from "graphql";
import type { H3Event } from "h3";
import type { AuthContext } from "../utils/auth";
import { clearAuthSession, setSession } from "../utils/auth";
import { prisma } from "../utils/prisma";
import { normalizePhone, verifyPassword } from "../utils/password";
import { persistChatMessage, runCrmAgentFor } from "../agent/crmAgent";
import { buildChangeSet, captureSnapshot, rollbackChangeSet } from "../utils/changeSet";
import type { ChangeSet } from "../utils/changeSet";

type GraphQLContext = {
  auth: AuthContext | null;
  event: H3Event;
};

function requireAuth(auth: AuthContext | null) {
  if (!auth) {
    throw new Error("Unauthorized");
  }
  return auth;
}

function mapChannel(channel: string) {
  if (channel === "TELEGRAM") return "Telegram";
  if (channel === "WHATSAPP") return "WhatsApp";
  if (channel === "INSTAGRAM") return "Instagram";
  if (channel === "EMAIL") return "Email";
  return "Phone";
}

function toDbChannel(channel: string) {
  const c = channel.toLowerCase();
  if (c === "telegram") return "TELEGRAM";
  if (c === "whatsapp") return "WHATSAPP";
  if (c === "instagram") return "INSTAGRAM";
  if (c === "email") return "EMAIL";
  return "PHONE";
}

async function loginWithPassword(event: H3Event, phoneInput: string, passwordInput: string) {
  const phone = normalizePhone(phoneInput);
  const password = (passwordInput ?? "").trim();

  if (!phone) {
    throw new Error("phone is required");
  }
  if (!password) {
    throw new Error("password is required");
  }

  const user = await prisma.user.findUnique({
    where: { phone },
    include: {
      memberships: {
        orderBy: { createdAt: "asc" },
        take: 1,
      },
    },
  });

  if (!user || !verifyPassword(password, user.passwordHash)) {
    throw new Error("invalid credentials");
  }

  const membership = user.memberships[0];
  if (!membership) {
    throw new Error("user has no team access");
  }

  const conversation =
    (await prisma.chatConversation.findFirst({
      where: { teamId: membership.teamId, createdByUserId: user.id },
      orderBy: { createdAt: "desc" },
    })) ||
    (await prisma.chatConversation.create({
      data: { teamId: membership.teamId, createdByUserId: user.id, title: "Pilot" },
    }));

  setSession(event, {
    teamId: membership.teamId,
    userId: user.id,
    conversationId: conversation.id,
  });

  return { ok: true };
}

async function getAuthPayload(auth: AuthContext | null) {
  const ctx = requireAuth(auth);
  const [user, team, conv] = await Promise.all([
    prisma.user.findUnique({ where: { id: ctx.userId } }),
    prisma.team.findUnique({ where: { id: ctx.teamId } }),
    prisma.chatConversation.findUnique({
      where: { id: ctx.conversationId },
      include: { messages: { orderBy: { createdAt: "desc" }, take: 1, select: { text: true, createdAt: true } } },
    }),
  ]);

  if (!user || !team || !conv) {
    throw new Error("Unauthorized");
  }

  return {
    user: { id: user.id, phone: user.phone, name: user.name },
    team: { id: team.id, name: team.name },
    conversation: {
      id: conv.id,
      title: conv.title ?? "New chat",
      createdAt: conv.createdAt.toISOString(),
      updatedAt: conv.updatedAt.toISOString(),
      lastMessageAt: conv.messages[0]?.createdAt?.toISOString?.() ?? null,
      lastMessageText: conv.messages[0]?.text ?? null,
    },
  };
}

function defaultConversationTitle(input?: string | null) {
  const value = (input ?? "").trim();
  if (value) return value;
  const stamp = new Intl.DateTimeFormat("en-GB", {
    day: "2-digit",
    month: "short",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date());
  return `Chat ${stamp}`;
}

async function getChatConversations(auth: AuthContext | null) {
  const ctx = requireAuth(auth);

  const items = await prisma.chatConversation.findMany({
    where: { teamId: ctx.teamId, createdByUserId: ctx.userId },
    include: {
      messages: {
        orderBy: { createdAt: "desc" },
        take: 1,
        select: { text: true, createdAt: true },
      },
    },
    take: 100,
  });

  return items
    .map((c) => ({
      id: c.id,
      title: defaultConversationTitle(c.title),
      createdAt: c.createdAt.toISOString(),
      updatedAt: c.updatedAt.toISOString(),
      lastMessageAt: c.messages[0]?.createdAt?.toISOString?.() ?? null,
      lastMessageText: c.messages[0]?.text ?? null,
    }))
    .sort((a, b) => {
      const aTime = a.lastMessageAt ?? a.updatedAt;
      const bTime = b.lastMessageAt ?? b.updatedAt;
      return bTime.localeCompare(aTime);
    });
}

async function createChatConversation(auth: AuthContext | null, event: H3Event, titleInput?: string | null) {
  const ctx = requireAuth(auth);

  const conversation = await prisma.chatConversation.create({
    data: {
      teamId: ctx.teamId,
      createdByUserId: ctx.userId,
      title: defaultConversationTitle(titleInput),
    },
  });

  setSession(event, {
    teamId: ctx.teamId,
    userId: ctx.userId,
    conversationId: conversation.id,
  });

  return {
    id: conversation.id,
    title: defaultConversationTitle(conversation.title),
    createdAt: conversation.createdAt.toISOString(),
    updatedAt: conversation.updatedAt.toISOString(),
    lastMessageAt: null,
    lastMessageText: null,
  };
}

async function selectChatConversation(auth: AuthContext | null, event: H3Event, id: string) {
  const ctx = requireAuth(auth);
  const convId = (id ?? "").trim();
  if (!convId) throw new Error("id is required");

  const conversation = await prisma.chatConversation.findFirst({
    where: {
      id: convId,
      teamId: ctx.teamId,
      createdByUserId: ctx.userId,
    },
    select: { id: true },
  });

  if (!conversation) throw new Error("conversation not found");

  setSession(event, {
    teamId: ctx.teamId,
    userId: ctx.userId,
    conversationId: conversation.id,
  });

  return { ok: true };
}

async function archiveChatConversation(auth: AuthContext | null, event: H3Event, id: string) {
  const ctx = requireAuth(auth);
  const convId = (id ?? "").trim();
  if (!convId) throw new Error("id is required");

  const conversation = await prisma.chatConversation.findFirst({
    where: {
      id: convId,
      teamId: ctx.teamId,
      createdByUserId: ctx.userId,
    },
    select: { id: true },
  });

  if (!conversation) throw new Error("conversation not found");

  const nextConversationId = await prisma.$transaction(async (tx) => {
    await tx.chatConversation.delete({ where: { id: conversation.id } });

    if (ctx.conversationId !== conversation.id) {
      return ctx.conversationId;
    }

    const created = await tx.chatConversation.create({
      data: { teamId: ctx.teamId, createdByUserId: ctx.userId, title: "Pilot" },
      select: { id: true },
    });
    return created.id;
  });

  setSession(event, {
    teamId: ctx.teamId,
    userId: ctx.userId,
    conversationId: nextConversationId,
  });

  return { ok: true };
}

async function getChatMessages(auth: AuthContext | null) {
  const ctx = requireAuth(auth);
  const items = await prisma.chatMessage.findMany({
    where: { teamId: ctx.teamId, conversationId: ctx.conversationId },
    orderBy: { createdAt: "asc" },
    take: 200,
  });

  return items.map((m) => {
    const cs = getChangeSetFromPlanJson(m.planJson);
    const messageKind = getMessageKindFromPlanJson(m.planJson) ?? (cs ? "change_set_summary" : null);
    return {
      id: m.id,
      role: m.role === "USER" ? "user" : m.role === "ASSISTANT" ? "assistant" : "system",
      text: m.text,
      messageKind,
      requestId: null,
      eventType: null,
      phase: null,
      transient: null,
      thinking: [],
      tools: [],
      toolRuns: [],
      changeSetId: cs?.id ?? null,
      changeStatus: cs?.status ?? null,
      changeSummary: cs?.summary ?? null,
      changeItems: Array.isArray(cs?.items)
        ? cs.items.map((item) => ({
            entity: String(item.entity ?? ""),
            action: String(item.action ?? ""),
            title: String(item.title ?? ""),
            before: String(item.before ?? ""),
            after: String(item.after ?? ""),
          }))
        : [],
      createdAt: m.createdAt.toISOString(),
    };
  });
}

async function getDashboard(auth: AuthContext | null) {
  const ctx = requireAuth(auth);
  const from = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
  const to = new Date(Date.now() + 1000 * 60 * 60 * 24 * 60);

  const [
    contactsRaw,
    communicationsRaw,
    calendarRaw,
    dealsRaw,
    feedRaw,
    pinsRaw,
    documentsRaw,
  ] = await Promise.all([
    prisma.contact.findMany({
      where: { teamId: ctx.teamId },
      include: {
        note: { select: { content: true } },
        messages: { select: { occurredAt: true }, orderBy: { occurredAt: "desc" }, take: 1 },
      },
      orderBy: { updatedAt: "desc" },
      take: 500,
    }),
    prisma.contactMessage.findMany({
      where: { contact: { teamId: ctx.teamId } },
      orderBy: { occurredAt: "asc" },
      take: 2000,
      include: { contact: { select: { id: true, name: true } } },
    }),
    prisma.calendarEvent.findMany({
      where: { teamId: ctx.teamId, startsAt: { gte: from, lte: to } },
      include: { contact: { select: { name: true } } },
      orderBy: { startsAt: "asc" },
      take: 500,
    }),
    prisma.deal.findMany({
      where: { teamId: ctx.teamId },
      include: {
        contact: { select: { name: true, company: true } },
        steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
      },
      orderBy: { updatedAt: "desc" },
      take: 500,
    }),
    prisma.feedCard.findMany({
      where: { teamId: ctx.teamId },
      include: { contact: { select: { name: true } } },
      orderBy: { happenedAt: "desc" },
      take: 200,
    }),
    prisma.contactPin.findMany({
      where: { teamId: ctx.teamId },
      include: { contact: { select: { name: true } } },
      orderBy: { updatedAt: "desc" },
      take: 500,
    }),
    prisma.workspaceDocument.findMany({
      where: { teamId: ctx.teamId },
      orderBy: { updatedAt: "desc" },
      take: 200,
    }),
  ]);

  const channelsByContactId = new Map<string, Set<string>>();
  for (const item of communicationsRaw) {
    if (!channelsByContactId.has(item.contactId)) {
      channelsByContactId.set(item.contactId, new Set());
    }
    channelsByContactId.get(item.contactId)?.add(mapChannel(item.channel));
  }

  const contacts = contactsRaw.map((c) => ({
    id: c.id,
    name: c.name,
    avatar: c.avatarUrl ?? "",
    company: c.company ?? "",
    country: c.country ?? "",
    location: c.location ?? "",
    channels: Array.from(channelsByContactId.get(c.id) ?? []),
    lastContactAt: c.messages[0]?.occurredAt?.toISOString?.() ?? c.updatedAt.toISOString(),
    description: c.note?.content ?? "",
  }));

  const communications = communicationsRaw.map((m) => ({
    id: m.id,
    at: m.occurredAt.toISOString(),
    contactId: m.contactId,
    contact: m.contact.name,
    channel: mapChannel(m.channel),
    kind: m.kind === "CALL" ? "call" : "message",
    direction: m.direction === "IN" ? "in" : "out",
    text: m.content,
    audioUrl: "",
    duration: m.durationSec ? new Date(m.durationSec * 1000).toISOString().slice(14, 19) : "",
    transcript: Array.isArray(m.transcriptJson) ? ((m.transcriptJson as any) as string[]) : [],
  }));

  const calendar = calendarRaw.map((e) => ({
    id: e.id,
    title: e.title,
    start: e.startsAt.toISOString(),
    end: (e.endsAt ?? e.startsAt).toISOString(),
    contact: e.contact?.name ?? "",
    note: e.note ?? "",
    isArchived: Boolean(e.isArchived),
    createdAt: e.createdAt.toISOString(),
    archiveNote: e.archiveNote ?? "",
    archivedAt: e.archivedAt?.toISOString() ?? "",
  }));

  const deals = dealsRaw.map((d) => ({
    id: d.id,
    contact: d.contact.name,
    title: d.title,
    company: d.contact.company ?? "",
    stage: d.stage,
    amount: d.amount ? String(d.amount) : "",
    nextStep: d.nextStep ?? "",
    summary: d.summary ?? "",
    currentStepId: d.currentStepId ?? "",
    steps: d.steps.map((step) => ({
      id: step.id,
      title: step.title,
      description: step.description ?? "",
      status: step.status,
      dueAt: step.dueAt?.toISOString() ?? "",
      order: step.order,
      completedAt: step.completedAt?.toISOString() ?? "",
    })),
  }));

  const feed = feedRaw.map((c) => ({
    id: c.id,
    at: c.happenedAt.toISOString(),
    contact: c.contact?.name ?? "",
    text: c.text,
    proposal: {
      title: ((c.proposalJson as any)?.title ?? "") as string,
      details: (Array.isArray((c.proposalJson as any)?.details) ? (c.proposalJson as any).details : []) as string[],
      key: ((c.proposalJson as any)?.key ?? "") as string,
    },
    decision: c.decision === "ACCEPTED" ? "accepted" : c.decision === "REJECTED" ? "rejected" : "pending",
    decisionNote: c.decisionNote ?? "",
  }));

  const pins = pinsRaw.map((p) => ({
    id: p.id,
    contact: p.contact.name,
    text: p.text,
  }));

  const documents = documentsRaw.map((d) => ({
    id: d.id,
    title: d.title,
    type: d.type,
    owner: d.owner,
    scope: d.scope,
    updatedAt: d.updatedAt.toISOString(),
    summary: d.summary,
    body: d.body,
  }));

  return {
    contacts,
    communications,
    calendar,
    deals,
    feed,
    pins,
    documents,
  };
}

async function createCalendarEvent(auth: AuthContext | null, input: {
  title: string;
  start: string;
  end?: string;
  contact?: string;
  note?: string;
  archived?: boolean;
  archiveNote?: string;
}) {
  const ctx = requireAuth(auth);

  const title = (input?.title ?? "").trim();
  const start = input?.start ? new Date(input.start) : null;
  const end = input?.end ? new Date(input.end) : null;

  if (!title) throw new Error("title is required");
  if (!start || Number.isNaN(start.getTime())) throw new Error("start is invalid");

  const contactName = (input?.contact ?? "").trim();
  const contact = contactName
    ? await prisma.contact.findFirst({ where: { teamId: ctx.teamId, name: contactName }, select: { id: true, name: true } })
    : null;

  const created = await prisma.calendarEvent.create({
    data: (() => {
      const archived = Boolean(input?.archived);
      const note = (input?.note ?? "").trim() || null;
      const archiveNote = (input?.archiveNote ?? "").trim() || note;
      return {
        teamId: ctx.teamId,
        contactId: contact?.id ?? null,
        title,
        startsAt: start,
        endsAt: end && !Number.isNaN(end.getTime()) ? end : null,
        note,
        isArchived: archived,
        archiveNote: archived ? archiveNote : null,
        archivedAt: archived ? new Date() : null,
      };
    })(),
    include: { contact: { select: { name: true } } },
  });

  return {
    id: created.id,
    title: created.title,
    start: created.startsAt.toISOString(),
    end: (created.endsAt ?? created.startsAt).toISOString(),
    contact: created.contact?.name ?? "",
    note: created.note ?? "",
    isArchived: Boolean(created.isArchived),
    createdAt: created.createdAt.toISOString(),
    archiveNote: created.archiveNote ?? "",
    archivedAt: created.archivedAt?.toISOString() ?? "",
  };
}

async function archiveCalendarEvent(auth: AuthContext | null, input: { id: string; archiveNote?: string }) {
  const ctx = requireAuth(auth);
  const id = String(input?.id ?? "").trim();
  const archiveNote = String(input?.archiveNote ?? "").trim();
  if (!id) throw new Error("id is required");

  const existing = await prisma.calendarEvent.findFirst({
    where: { id, teamId: ctx.teamId },
    select: { id: true },
  });
  if (!existing) throw new Error("event not found");

  const updated = await prisma.calendarEvent.update({
    where: { id },
    data: {
      isArchived: true,
      archiveNote: archiveNote || null,
      archivedAt: new Date(),
    },
    include: { contact: { select: { name: true } } },
  });

  return {
    id: updated.id,
    title: updated.title,
    start: updated.startsAt.toISOString(),
    end: (updated.endsAt ?? updated.startsAt).toISOString(),
    contact: updated.contact?.name ?? "",
    note: updated.note ?? "",
    isArchived: Boolean(updated.isArchived),
    createdAt: updated.createdAt.toISOString(),
    archiveNote: updated.archiveNote ?? "",
    archivedAt: updated.archivedAt?.toISOString() ?? "",
  };
}

async function createCommunication(auth: AuthContext | null, input: {
  contact: string;
  channel?: string;
  kind?: "message" | "call";
  direction?: "in" | "out";
  text?: string;
  audioUrl?: string;
  at?: string;
  durationSec?: number;
  transcript?: string[];
}) {
  const ctx = requireAuth(auth);

  const contactName = (input?.contact ?? "").trim();
  if (!contactName) throw new Error("contact is required");

  const contact = await prisma.contact.findFirst({
    where: { teamId: ctx.teamId, name: contactName },
    select: { id: true },
  });
  if (!contact) throw new Error("contact not found");

  const occurredAt = input?.at ? new Date(input.at) : new Date();
  if (Number.isNaN(occurredAt.getTime())) throw new Error("at is invalid");

  const created = await prisma.contactMessage.create({
    data: {
      contactId: contact.id,
      kind: input?.kind === "call" ? "CALL" : "MESSAGE",
      direction: input?.direction === "in" ? "IN" : "OUT",
      channel: toDbChannel(input?.channel ?? "Phone") as any,
      content: (input?.text ?? "").trim(),
      durationSec: typeof input?.durationSec === "number" ? input.durationSec : null,
      transcriptJson: Array.isArray(input?.transcript) ? input.transcript : undefined,
      occurredAt,
    },
  });

  return { ok: true, id: created.id };
}

async function updateCommunicationTranscript(auth: AuthContext | null, id: string, transcript: string[]) {
  const ctx = requireAuth(auth);
  const messageId = String(id ?? "").trim();
  if (!messageId) throw new Error("id is required");

  const lines = Array.isArray(transcript)
    ? transcript.map((line) => String(line ?? "").trim()).filter(Boolean)
    : [];

  const updated = await prisma.contactMessage.updateMany({
    where: {
      id: messageId,
      contact: { teamId: ctx.teamId },
    },
    data: {
      transcriptJson: lines,
    },
  });

  if (!updated.count) throw new Error("communication not found");
  return { ok: true, id: messageId };
}

async function updateFeedDecision(auth: AuthContext | null, id: string, decision: "accepted" | "rejected" | "pending", decisionNote?: string) {
  const ctx = requireAuth(auth);

  if (!id) throw new Error("id is required");
  if (!decision) throw new Error("decision is required");

  const nextDecision = decision === "accepted" ? "ACCEPTED" : decision === "rejected" ? "REJECTED" : "PENDING";

  const res = await prisma.feedCard.updateMany({
    where: { id, teamId: ctx.teamId },
    data: { decision: nextDecision, decisionNote: decisionNote ?? null },
  });
  if (res.count === 0) throw new Error("feed card not found");

  return { ok: true, id };
}

function getChangeSetFromPlanJson(planJson: unknown): ChangeSet | null {
  const debug = (planJson as any) ?? {};
  const cs = debug?.changeSet;
  if (!cs || typeof cs !== "object") return null;
  if (!cs.id || !Array.isArray(cs.items) || !Array.isArray(cs.undo)) return null;
  return cs as ChangeSet;
}

function getMessageKindFromPlanJson(planJson: unknown): string | null {
  const debug = (planJson as any) ?? {};
  const kind = debug?.messageKind;
  if (!kind || typeof kind !== "string") return null;
  return kind;
}

function renderChangeSetSummary(changeSet: ChangeSet): string {
  const totals = { created: 0, updated: 0, deleted: 0 };
  for (const item of changeSet.items) {
    if (item.action === "created") totals.created += 1;
    else if (item.action === "updated") totals.updated += 1;
    else if (item.action === "deleted") totals.deleted += 1;
  }

  return [
    "Technical change summary",
    `Total: ${changeSet.items.length} · Created: ${totals.created} · Updated: ${totals.updated} · Archived: ${totals.deleted}`,
  ].join("\n");
}

async function findLatestChangeCarrierMessage(auth: AuthContext | null) {
  const ctx = requireAuth(auth);
  const items = await prisma.chatMessage.findMany({
    where: {
      teamId: ctx.teamId,
      conversationId: ctx.conversationId,
      role: "ASSISTANT",
    },
    orderBy: { createdAt: "desc" },
    take: 30,
  });

  for (const item of items) {
    const changeSet = getChangeSetFromPlanJson(item.planJson);
    if (!changeSet) continue;
    if (changeSet.status === "rolled_back") continue;
    return { item, changeSet };
  }
  return null;
}

async function confirmLatestChangeSet(auth: AuthContext | null) {
  const found = await findLatestChangeCarrierMessage(auth);
  if (!found) return { ok: true };

  const { item, changeSet } = found;
  if (changeSet.status === "confirmed") return { ok: true };

  const next = {
    ...((item.planJson as any) ?? {}),
    changeSet: {
      ...changeSet,
      status: "confirmed",
      confirmedAt: new Date().toISOString(),
    },
  };

  await prisma.chatMessage.update({
    where: { id: item.id },
    data: { planJson: next as any },
  });

  return { ok: true };
}

async function rollbackLatestChangeSet(auth: AuthContext | null) {
  const ctx = requireAuth(auth);
  const found = await findLatestChangeCarrierMessage(ctx);
  if (!found) return { ok: true };

  const { item, changeSet } = found;
  if (changeSet.status === "rolled_back") return { ok: true };

  await rollbackChangeSet(prisma, ctx.teamId, changeSet);

  const next = {
    ...((item.planJson as any) ?? {}),
    changeSet: {
      ...changeSet,
      status: "rolled_back",
      rolledBackAt: new Date().toISOString(),
    },
  };

  await prisma.chatMessage.update({
    where: { id: item.id },
    data: { planJson: next as any },
  });

  return { ok: true };
}

async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
  const ctx = requireAuth(auth);
  const text = (textInput ?? "").trim();
  if (!text) throw new Error("text is required");
  const requestId = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;

  const snapshotBefore = await captureSnapshot(prisma, ctx.teamId);

  await persistChatMessage({
    teamId: ctx.teamId,
    conversationId: ctx.conversationId,
    authorUserId: ctx.userId,
    role: "USER",
    text,
    requestId,
    eventType: "user",
    phase: "final",
    transient: false,
  });

  const reply = await runCrmAgentFor({
    teamId: ctx.teamId,
    userId: ctx.userId,
    userText: text,
    requestId,
    conversationId: ctx.conversationId,
    onTrace: async () => {},
  });

  const snapshotAfter = await captureSnapshot(prisma, ctx.teamId);
  const changeSet = buildChangeSet(snapshotBefore, snapshotAfter);

  await persistChatMessage({
    teamId: ctx.teamId,
    conversationId: ctx.conversationId,
    authorUserId: null,
    role: "ASSISTANT",
    text: reply.text,
    requestId,
    eventType: "assistant",
    phase: "final",
    transient: false,
  });

  if (changeSet) {
    await persistChatMessage({
      teamId: ctx.teamId,
      conversationId: ctx.conversationId,
      authorUserId: null,
      role: "ASSISTANT",
      text: renderChangeSetSummary(changeSet),
      requestId,
      eventType: "note",
      phase: "final",
      transient: false,
      messageKind: "change_set_summary",
      changeSet,
    });
  }

  return { ok: true };
}

async function logPilotNote(auth: AuthContext | null, textInput: string) {
  const ctx = requireAuth(auth);
  const text = (textInput ?? "").trim();
  if (!text) throw new Error("text is required");

  await persistChatMessage({
    teamId: ctx.teamId,
    conversationId: ctx.conversationId,
    authorUserId: null,
    role: "ASSISTANT",
    text,
  });

  return { ok: true };
}

async function toggleContactPin(auth: AuthContext | null, contactInput: string, textInput: string) {
  const ctx = requireAuth(auth);
  const contactName = (contactInput ?? "").trim();
  const text = (textInput ?? "").replace(/\s+/g, " ").trim();
  if (!contactName) throw new Error("contact is required");
  if (!text) throw new Error("text is required");

  const contact = await prisma.contact.findFirst({
    where: { teamId: ctx.teamId, name: contactName },
    select: { id: true },
  });
  if (!contact) throw new Error("contact not found");

  const existing = await prisma.contactPin.findFirst({
    where: { teamId: ctx.teamId, contactId: contact.id, text },
    select: { id: true },
  });

  if (existing) {
    await prisma.contactPin.deleteMany({
      where: { teamId: ctx.teamId, contactId: contact.id, text },
    });
    return { ok: true, pinned: false };
  }

  await prisma.contactPin.create({
    data: {
      teamId: ctx.teamId,
      contactId: contact.id,
      text,
    },
  });
  return { ok: true, pinned: true };
}

export const crmGraphqlSchema = buildSchema(`
  type Query {
    me: MePayload!
    chatMessages: [PilotMessage!]!
    chatConversations: [Conversation!]!
    dashboard: DashboardPayload!
  }

  type Mutation {
    login(phone: String!, password: String!): MutationResult!
    logout: MutationResult!
    createChatConversation(title: String): Conversation!
    selectChatConversation(id: ID!): MutationResult!
    archiveChatConversation(id: ID!): MutationResult!
    sendPilotMessage(text: String!): MutationResult!
    confirmLatestChangeSet: MutationResult!
    rollbackLatestChangeSet: MutationResult!
    logPilotNote(text: String!): MutationResult!
    toggleContactPin(contact: String!, text: String!): PinToggleResult!
    createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
    archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
    createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
    updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
    updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
  }

  type MutationResult {
    ok: Boolean!
  }

  type MutationWithIdResult {
    ok: Boolean!
    id: ID!
  }

  type PinToggleResult {
    ok: Boolean!
    pinned: Boolean!
  }

  input CreateCalendarEventInput {
    title: String!
    start: String!
    end: String
    contact: String
    note: String
    archived: Boolean
    archiveNote: String
  }

  input ArchiveCalendarEventInput {
    id: ID!
    archiveNote: String
  }

  input CreateCommunicationInput {
    contact: String!
    channel: String
    kind: String
    direction: String
    text: String
    audioUrl: String
    at: String
    durationSec: Int
    transcript: [String!]
  }

  type MePayload {
    user: MeUser!
    team: MeTeam!
    conversation: Conversation!
  }

  type MeUser {
    id: ID!
    phone: String!
    name: String!
  }

  type MeTeam {
    id: ID!
    name: String!
  }

  type Conversation {
    id: ID!
    title: String!
    createdAt: String!
    updatedAt: String!
    lastMessageAt: String
    lastMessageText: String
  }

  type PilotMessage {
    id: ID!
    role: String!
    text: String!
    messageKind: String
    requestId: String
    eventType: String
    phase: String
    transient: Boolean
    thinking: [String!]!
    tools: [String!]!
    toolRuns: [PilotToolRun!]!
    changeSetId: String
    changeStatus: String
    changeSummary: String
    changeItems: [PilotChangeItem!]!
    createdAt: String!
  }

  type PilotChangeItem {
    entity: String!
    action: String!
    title: String!
    before: String!
    after: String!
  }

  type PilotToolRun {
    name: String!
    status: String!
    input: String!
    output: String!
    at: String!
  }

  type DashboardPayload {
    contacts: [Contact!]!
    communications: [CommItem!]!
    calendar: [CalendarEvent!]!
    deals: [Deal!]!
    feed: [FeedCard!]!
    pins: [CommPin!]!
    documents: [WorkspaceDocument!]!
  }

  type Contact {
    id: ID!
    name: String!
    avatar: String!
    company: String!
    country: String!
    location: String!
    channels: [String!]!
    lastContactAt: String!
    description: String!
  }

  type CommItem {
    id: ID!
    at: String!
    contactId: String!
    contact: String!
    channel: String!
    kind: String!
    direction: String!
    text: String!
    audioUrl: String!
    duration: String!
    transcript: [String!]!
  }

  type CalendarEvent {
    id: ID!
    title: String!
    start: String!
    end: String!
    contact: String!
    note: String!
    isArchived: Boolean!
    createdAt: String!
    archiveNote: String!
    archivedAt: String!
  }

  type Deal {
    id: ID!
    contact: String!
    title: String!
    company: String!
    stage: String!
    amount: String!
    nextStep: String!
    summary: String!
    currentStepId: String!
    steps: [DealStep!]!
  }

  type DealStep {
    id: ID!
    title: String!
    description: String!
    status: String!
    dueAt: String!
    order: Int!
    completedAt: String!
  }

  type FeedCard {
    id: ID!
    at: String!
    contact: String!
    text: String!
    proposal: FeedProposal!
    decision: String!
    decisionNote: String!
  }

  type FeedProposal {
    title: String!
    details: [String!]!
    key: String!
  }

  type CommPin {
    id: ID!
    contact: String!
    text: String!
  }

  type WorkspaceDocument {
    id: ID!
    title: String!
    type: String!
    owner: String!
    scope: String!
    updatedAt: String!
    summary: String!
    body: String!
  }
`);

export const crmGraphqlRoot = {
  me: async (_args: unknown, context: GraphQLContext) => getAuthPayload(context.auth),
  chatMessages: async (_args: unknown, context: GraphQLContext) => getChatMessages(context.auth),
  chatConversations: async (_args: unknown, context: GraphQLContext) => getChatConversations(context.auth),
  dashboard: async (_args: unknown, context: GraphQLContext) => getDashboard(context.auth),

  login: async (args: { phone: string; password: string }, context: GraphQLContext) =>
    loginWithPassword(context.event, args.phone, args.password),

  logout: async (_args: unknown, context: GraphQLContext) => {
    clearAuthSession(context.event);
    return { ok: true };
  },

  createChatConversation: async (args: { title?: string }, context: GraphQLContext) =>
    createChatConversation(context.auth, context.event, args.title),

  selectChatConversation: async (args: { id: string }, context: GraphQLContext) =>
    selectChatConversation(context.auth, context.event, args.id),

  archiveChatConversation: async (args: { id: string }, context: GraphQLContext) =>
    archiveChatConversation(context.auth, context.event, args.id),

  sendPilotMessage: async (args: { text: string }, context: GraphQLContext) =>
    sendPilotMessage(context.auth, args.text),

  confirmLatestChangeSet: async (_args: unknown, context: GraphQLContext) =>
    confirmLatestChangeSet(context.auth),

  rollbackLatestChangeSet: async (_args: unknown, context: GraphQLContext) =>
    rollbackLatestChangeSet(context.auth),

  logPilotNote: async (args: { text: string }, context: GraphQLContext) =>
    logPilotNote(context.auth, args.text),

  toggleContactPin: async (args: { contact: string; text: string }, context: GraphQLContext) =>
    toggleContactPin(context.auth, args.contact, args.text),

  createCalendarEvent: async (args: { input: { title: string; start: string; end?: string; contact?: string; note?: string; archived?: boolean; archiveNote?: string } }, context: GraphQLContext) =>
    createCalendarEvent(context.auth, args.input),

  archiveCalendarEvent: async (args: { input: { id: string; archiveNote?: string } }, context: GraphQLContext) =>
    archiveCalendarEvent(context.auth, args.input),

  createCommunication: async (
    args: {
      input: {
        contact: string;
        channel?: string;
        kind?: "message" | "call";
        direction?: "in" | "out";
        text?: string;
        audioUrl?: string;
        at?: string;
        durationSec?: number;
        transcript?: string[];
      };
    },
    context: GraphQLContext,
  ) => createCommunication(context.auth, args.input),

  updateCommunicationTranscript: async (
    args: { id: string; transcript: string[] },
    context: GraphQLContext,
  ) => updateCommunicationTranscript(context.auth, args.id, args.transcript),

  updateFeedDecision: async (
    args: { id: string; decision: "accepted" | "rejected" | "pending"; decisionNote?: string },
    context: GraphQLContext,
  ) => updateFeedDecision(context.auth, args.id, args.decision, args.decisionNote),
};
