import { streamChat } from "@/lib/vllm";
import { sendMessage } from "@/lib/synap";
import { assembleBrief } from "@/lib/briefing";
import {
  addPersonalizationHints,
  ensurePersonalizationSession,
  ensurePersonalizationUser,
  recordObserverPersonalization,
} from "@/lib/tidal-personalization";
import type { ObserverOutput } from "@/lib/types";

interface ChatBody {
  messages: { role: "user" | "assistant"; content: string }[];
  conversationId?: string;
  personId?: string;
}

// --- Per-conversation state (in-memory, lost on restart — fine for M3) ---
const turnCounts = new Map<string, number>();
const signalBuffers = new Map<string, ObserverOutput[]>();
const lastTopics = new Map<string, string>();

const SYNTHESIS_INTERVAL = 5;

export async function POST(req: Request) {
  const requestTimestamp = Date.now();

  let body: ChatBody;
  try {
    body = await req.json();
  } catch {
    return new Response("Invalid JSON", { status: 400 });
  }

  if (!body.messages?.length) {
    return new Response("No messages provided", { status: 400 });
  }

  const lastUserMsg = body.messages.findLast((m) => m.role === "user");
  const conversationId = body.conversationId;
  const personId = body.personId;

  // Keep tidal personalization state hot, but never block chat if unavailable.
  if (personId) {
    ensurePersonalizationUser(personId).catch((err) =>
      console.error("[tidal] ensure user failed:", err)
    );
  }
  if (personId && conversationId) {
    ensurePersonalizationSession(conversationId, personId).catch((err) =>
      console.error("[tidal] ensure session failed:", err)
    );
  }

  // 1. Store user message in Synap (non-blocking — don't delay stream start)
  if (conversationId && lastUserMsg) {
    sendMessage("user", lastUserMsg.content, conversationId).catch((err) =>
      console.error("[synap] failed to store user message:", err.message)
    );
  }

  // 2. Assemble communication brief (replaces scatter-shot recall + cohort loading)
  const synapBrief = personId
    ? await assembleBrief(personId).catch((err) => {
        console.error("[brief] assembly failed:", err.message);
        return undefined;
      })
    : undefined;
  const brief = personId
    ? await addPersonalizationHints(personId, synapBrief)
    : synapBrief;

  const encoder = new TextEncoder();
  let fullResponse = "";

  const stream = new ReadableStream({
    async start(controller) {
      try {
        for await (const token of streamChat(body.messages, brief)) {
          fullResponse += token;
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ token })}\n\n`)
          );
        }
        controller.enqueue(encoder.encode("data: [DONE]\n\n"));
      } catch (err) {
        const message =
          err instanceof Error ? err.message : "Connection failed";
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ error: message })}\n\n`)
        );
      } finally {
        controller.close();

        // 5. Store assistant response in Synap (after stream ends)
        if (conversationId && fullResponse) {
          sendMessage("aeries", fullResponse, conversationId).catch((err) =>
            console.error(
              "[synap] failed to store assistant message:",
              err.message
            )
          );
        }

        // 6. Fire deep observer (non-blocking)
        if (conversationId && lastUserMsg && fullResponse) {
          fireDeepObserver(
            lastUserMsg.content,
            fullResponse,
            conversationId,
            requestTimestamp,
            body.messages,
            personId
          ).catch((err) =>
            console.error("[observe] failed:", err.message)
          );
        }
      }
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

/** Deep observer: Tier 1 structured extraction + Tier 2 periodic synthesis + profile update. */
async function fireDeepObserver(
  userMessage: string,
  assistantMessage: string,
  conversationId: string,
  requestTimestamp: number,
  allMessages: { role: string; content: string }[],
  personId?: string
): Promise<void> {
  const {
    extractObserverOutput,
    outputToSignalMemories,
    storeSignals,
    synthesizeObservations,
    storeObservations,
  } = await import("@/lib/observer");

  // Track turns
  const turn = (turnCounts.get(conversationId) ?? 0) + 1;
  turnCounts.set(conversationId, turn);

  // Compute latency (time between request arrival and now — approximation)
  const latencySeconds = Math.round((Date.now() - requestTimestamp) / 1000);

  // Tier 1: Structured extraction
  const output = await extractObserverOutput(userMessage, assistantMessage, {
    turnNumber: turn,
    latencySeconds,
    previousTopic: lastTopics.get(conversationId),
  });

  if (!output) {
    console.log(`[observer] turn ${turn}: extraction returned null`);
    return;
  }

  console.log(
    `[observer] turn ${turn}: topic=${output.topic.primary}, sentiment=${output.engagement.sentiment_score}, formality=${output.style.formality}`
  );

  // Track topic for next turn's context
  lastTopics.set(conversationId, output.topic.primary);

  // Store dimension-tagged signals in Synap (with person tag)
  const memories = outputToSignalMemories(output, conversationId, personId);
  await storeSignals(memories);

  // Buffer for synthesis
  const buffer = signalBuffers.get(conversationId) ?? [];
  buffer.push(output);
  // Keep only last 10 to bound memory
  if (buffer.length > 10) buffer.shift();
  signalBuffers.set(conversationId, buffer);

  // Tier 2: Periodic synthesis (every N turns)
  if (turn % SYNTHESIS_INTERVAL === 0 && buffer.length >= 3) {
    console.log(`[observer] turn ${turn}: running Tier 2 synthesis`);

    // Build conversation snippet from last few messages
    const snippet = allMessages
      .slice(-6)
      .map((m) => `${m.role}: ${m.content.slice(0, 100)}`)
      .join("\n");

    const observations = await synthesizeObservations(buffer, snippet);
    if (observations.length) {
      await storeObservations(observations, conversationId, personId);
    }
  }

  if (personId) {
    await recordObserverPersonalization({
      personId,
      conversationId,
      turn,
      assistantMessage,
      output,
    }).catch((err) =>
      console.error("[tidal] observer personalization write failed:", err)
    );
  }

  // M4: Update person profile after signal extraction
  if (personId) {
    const { computeProfile, storeProfile, loadProfile } = await import(
      "@/lib/cohorts"
    );

    const existing = await loadProfile(personId);
    const updated = computeProfile([output], existing, personId);

    console.log(
      `[cohorts] updated profile for ${personId.slice(0, 8)}…: interactions=${updated.interactionCount}, cohorts=[${updated.cohorts.map((c) => `${c.cohort}(${c.probability.toFixed(2)})`).join(", ")}]`
    );

    await storeProfile(updated);
  }
}
