import { Context, Effect, Layer } from "effect";
import type { Database } from "bun:sqlite";
import type { AgentEvent } from "@reactive-agents/core";
import {
  insertEvent,
  getRunAgentId,
  getGatewayAgent,
  upsertRun,
  updateRunStats,
  getNextSeq,
} from "../db/queries.js";
import { enforceRetention } from "../db/schema.js";
import { CortexEventBridge } from "./event-bridge.js";
import {
  isLifecycleEvent,
  parseWebhookConfigs,
  selectWebhookTargets,
  buildWebhookPayload,
  dispatchWebhooks,
} from "./webhook-dispatch.js";
import type { CortexIngestMessage, CortexLiveMessage } from "../types.js";
import { CORTEX_DESK_LIVE_AGENT_ID } from "../types.js";
import { CortexError } from "../errors.js";

export class CortexIngestService extends Context.Tag("CortexIngestService")<
  CortexIngestService,
  {
    readonly handleEvent: (
      agentId: string,
      runId: string,
      msg: CortexIngestMessage,
    ) => Effect.Effect<void, CortexError>;
    readonly getSubscriberCount: (agentId: string) => Effect.Effect<number, never>;
  }
>() {}

const deriveRunStats = (
  event: AgentEvent,
): Partial<{
  iterationCount: number;
  tokensUsed: number;
  tokensUsedTotal: number;
  cost: number;
  status: string;
  completedAt: number;
  debrief: string;
  provider: string;
  model: string;
  strategy: string;
}> => {
  // AgentStarted fires first — definitive provider + model for the run
  if (event._tag === "AgentStarted") {
    const e = event as { provider?: string; model?: string };
    return {
      ...(typeof e.provider === "string" && e.provider ? { provider: e.provider } : {}),
      ...(typeof e.model    === "string" && e.model    ? { model:    e.model    } : {}),
    };
  }
  if (event._tag === "LLMRequestCompleted") {
    const e = event as { tokensUsed?: number; estimatedCost?: number; provider?: string; model?: string };
    return {
      tokensUsed: e.tokensUsed ?? 0,
      cost: e.estimatedCost ?? 0,
      // Override with actual model used (may differ from AgentStarted if routing changed it)
      ...(typeof e.provider === "string" && e.provider && e.provider !== "unknown" ? { provider: e.provider } : {}),
      ...(typeof e.model    === "string" && e.model    && e.model    !== "unknown" ? { model:    e.model    } : {}),
    };
  }
  if (event._tag === "ReasoningStepCompleted") {
    const e = event as { totalSteps?: number; step?: number; strategy?: string };
    return {
      iterationCount: e.totalSteps ?? e.step ?? 0,
      ...(typeof e.strategy === "string" && e.strategy ? { strategy: e.strategy } : {}),
    };
  }
  if (event._tag === "ReasoningIterationProgress") {
    const e = event as { iteration?: number; strategy?: string };
    return {
      iterationCount: e.iteration ?? 0,
      ...(typeof e.strategy === "string" && e.strategy ? { strategy: e.strategy } : {}),
    };
  }
  if (event._tag === "AgentCompleted") {
    const e = event as { success?: boolean; totalTokens?: number; durationMs?: number; error?: string };
    // Only explicit false is failure — omitted `success` is treated as completed (matches desk UX; some paths omit the flag).
    const status = e.success === false ? "failed" : "completed";
    return {
      status,
      completedAt: Date.now(),
      // totalTokens is the authoritative count — persisted via MAX so it doesn't
      // overcount when added to per-call accumulated values.
      ...(typeof e.totalTokens === "number" && e.totalTokens > 0
        ? { tokensUsedTotal: e.totalTokens }
        : {}),
      ...(typeof e.error === "string" && e.error ? { errorMessage: e.error } : {}),
    };
  }
  if (event._tag === "TaskFailed") {
    const e = event as { error?: string; reason?: string };
    const errorMessage = e.error ?? e.reason;
    return {
      status: "failed",
      completedAt: Date.now(),
      ...(typeof errorMessage === "string" && errorMessage ? { errorMessage } : {}),
    };
  }
  if (event._tag === "DebriefCompleted") {
    const e = event as { debrief: unknown };
    return { debrief: JSON.stringify(e.debrief ?? null) };
  }
  return {};
};

const IGNORED_INTERNAL_RUN_IDS = new Set([
  "unknown",
  "structured-output",
  "classify-tool-relevance",
  "llm-direct",
]);

function shouldIgnoreRunId(runId: string): boolean {
  return IGNORED_INTERNAL_RUN_IDS.has(runId);
}

export const CortexIngestServiceLive = (db: Database) =>
  Layer.effect(
    CortexIngestService,
    Effect.gen(function* () {
      const bridge = yield* CortexEventBridge;

      return {
        handleEvent: (agentId, runId, msg) =>
          Effect.gen(function* () {
            // Ignore known internal pseudo-task streams that pollute Stage.
            if (shouldIgnoreRunId(runId)) return;

            yield* Effect.sync(() => {
              const existingAgentId = getRunAgentId(db, runId);
              const eventAgentId =
                "agentId" in msg.event && typeof msg.event.agentId === "string"
                  ? msg.event.agentId
                  : undefined;

              // Canonicalize all events for the same run to one stable agent id.
              const normalizedAgentId =
                existingAgentId ??
                eventAgentId ??
                (agentId === "unknown" || agentId === runId ? undefined : agentId) ??
                runId;

              upsertRun(db, normalizedAgentId, runId);
              const seq = getNextSeq(db, runId);
              const normalizedMsg: CortexIngestMessage = {
                ...msg,
                agentId: normalizedAgentId,
              };
              insertEvent(db, normalizedMsg, seq);
              const patch = deriveRunStats(msg.event);
              if (Object.keys(patch).length > 0) updateRunStats(db, runId, patch);
              return normalizedAgentId;
            }).pipe(
              Effect.flatMap((normalizedAgentId) => {
                const liveMsg: CortexLiveMessage = {
                  v: 1,
                  ts: Date.now(),
                  agentId: normalizedAgentId,
                  runId,
                  source: "eventbus",
                  type: msg.event._tag,
                  payload: msg.event as unknown as Record<string, unknown>,
                };
                return Effect.all([
                  bridge.broadcast(normalizedAgentId, liveMsg),
                  bridge.broadcast(CORTEX_DESK_LIVE_AGENT_ID, liveMsg),
                ]).pipe(Effect.asVoid);
              }),
            );

            yield* Effect.sync(() => {
              const canonicalAgentId = getRunAgentId(db, runId) ?? agentId;
              const count = db
                .prepare("SELECT COUNT(*) as c FROM cortex_events WHERE agent_id = ?")
                .get(canonicalAgentId) as { c: number } | null;
              if ((count?.c ?? 0) % 100 === 0) enforceRetention(db, canonicalAgentId);

              // Lifecycle webhooks — fire-and-forget for saved agents that configured them.
              // Gated to lifecycle tags first so the common (per-step) event never hits the DB.
              if (isLifecycleEvent(msg.event._tag)) {
                const row = getGatewayAgent(db, canonicalAgentId);
                if (row) {
                  let cfg: unknown;
                  try { cfg = JSON.parse(row.config); } catch { cfg = null; }
                  const webhooks = parseWebhookConfigs(
                    cfg && typeof cfg === "object"
                      ? (cfg as { lifecycleWebhooks?: unknown }).lifecycleWebhooks
                      : undefined,
                  );
                  const targets = selectWebhookTargets(webhooks, msg.event._tag);
                  if (targets.length > 0) {
                    dispatchWebhooks(
                      targets,
                      buildWebhookPayload({
                        agentId: canonicalAgentId,
                        runId,
                        eventTag: msg.event._tag,
                        event: msg.event as unknown as Record<string, unknown>,
                      }),
                    );
                  }
                }
              }
            });
          }).pipe(
            Effect.catchAll((e) => Effect.fail(new CortexError({ message: String(e), cause: e }))),
          ),

        getSubscriberCount: (agentId) => bridge.subscriberCount(agentId),
      };
    }),
  );
