import type { TreeStreamDeltaEvent } from "./tree-delta";

export type TreeStreamScope = "workspace" | "subtree";
export type TreeStreamProjection = "sidebar_tree" | "page_tree";
export type TreeStreamEventName = "snapshot" | "delta" | "resync";

export interface TreeStreamCommandLogCursorRow {
  id?: string | null;
  command_id?: string | null;
  commandId?: string | null;
  created_at?: string | null;
  command_name?: string | null;
  commandName?: string | null;
  payload?: unknown;
}

export interface TreeStreamOverview {
  command_logs?: TreeStreamCommandLogCursorRow[] | null;
  domain_events?: unknown[] | null;
  next_cursor?: string | null;
  has_more?: boolean | null;
  generated_at?: string | null;
}

export interface TreeStreamSnapshotPayload {
  requestId: string;
  traceId: string;
  data: unknown;
  snapshot: unknown;
}

export interface TreeStreamEnvelope {
  kind: TreeStreamEventName;
  stream: TreeStreamScope;
  workspaceId: string;
  rootNodeId: string | null;
  cursor: string | null;
  projection: TreeStreamProjection;
  requestId: string;
  traceId: string;
  data: unknown;
  snapshot: unknown;
  overview: TreeStreamOverview;
}

export interface TreeStreamFrame {
  event: TreeStreamEventName;
  payload: TreeStreamEnvelope;
}

export interface StreamTreeFramesInput {
  workspaceId: string;
  rootNodeId?: string | null;
  initialCursor?: string | null;
  pollMs?: number;
  maxPolls?: number | null;
  loadOverview: () => Promise<TreeStreamOverview>;
  loadSnapshot: () => Promise<TreeStreamSnapshotPayload>;
  sleep?: (ms: number) => Promise<void>;
}

type DecodedTreeStreamCursor = {
  createdAt: string;
  id: string;
};

type TreeStreamDomainEventCursorRow = {
  id?: string | null;
  event_id?: string | null;
  command_id?: string | null;
  commandId?: string | null;
  created_at?: string | null;
  createdAt?: string | null;
  payload?: unknown;
};

const TREE_STREAM_NOOP_COMMANDS = new Set([
  "page.body.save",
  "page.layout.updateOptions",
  "documents.stats.update",
  "blocks.patch",
  "blocks.move",
  "blocks.embed",
]);

function normalizeNodeId(value: string | null | undefined) {
  const normalized = typeof value === "string" ? value.trim() : "";
  return normalized || null;
}

export function resolveTreeStreamContract(input: {
  rootNodeId?: string | null;
}): {
  stream: TreeStreamScope;
  projection: TreeStreamProjection;
  rootNodeId: string | null;
} {
  const rootNodeId = normalizeNodeId(input.rootNodeId);
  if (rootNodeId) {
    return {
      stream: "subtree",
      projection: "page_tree",
      rootNodeId,
    };
  }
  return {
    stream: "workspace",
    projection: "sidebar_tree",
    rootNodeId: null,
  };
}

export function encodeTreeStreamCursor(row: TreeStreamCommandLogCursorRow | null | undefined) {
  const id = typeof row?.id === "string" ? row.id.trim() : "";
  const createdAt = typeof row?.created_at === "string" ? row.created_at.trim() : "";
  if (!id || !createdAt) {
    return null;
  }
  return JSON.stringify({
    createdAt,
    id,
  });
}

function encodeTreeStreamDomainEventCursor(
  row: TreeStreamDomainEventCursorRow | null | undefined,
) {
  const rawId =
    typeof row?.event_id === "string"
      ? row.event_id.trim()
      : typeof row?.id === "string"
        ? row.id.trim()
        : "";
  const createdAt =
    typeof row?.created_at === "string"
      ? row.created_at.trim()
      : typeof row?.createdAt === "string"
        ? row.createdAt.trim()
        : "";
  if (!rawId || !createdAt) {
    return null;
  }
  return JSON.stringify({
    createdAt,
    id: `domain_event:${rawId}`,
  });
}

function decodeTreeStreamCursor(raw: string | null | undefined): DecodedTreeStreamCursor | null {
  if (!raw) {
    return null;
  }
  try {
    const parsed = JSON.parse(raw) as {
      createdAt?: string | null;
      id?: string | null;
    };
    const createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt.trim() : "";
    const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
    return createdAt && id ? { createdAt, id } : null;
  } catch {
    return null;
  }
}

export function resolveOverviewCursor(
  overview: TreeStreamOverview,
  fallback?: string | null,
) {
  const commandRows = Array.isArray(overview.command_logs) ? overview.command_logs : [];
  const eventRows = Array.isArray(overview.domain_events)
    ? (overview.domain_events as TreeStreamDomainEventCursorRow[])
    : [];
  const commandCursor = encodeTreeStreamCursor(commandRows[0] ?? null);
  const domainEventCursor = encodeTreeStreamDomainEventCursor(eventRows[0] ?? null);

  if (!commandCursor) {
    return domainEventCursor ?? fallback ?? null;
  }
  if (!domainEventCursor) {
    return commandCursor ?? fallback ?? null;
  }

  const decodedCommandCursor = decodeTreeStreamCursor(commandCursor);
  const decodedDomainEventCursor = decodeTreeStreamCursor(domainEventCursor);
  if (!decodedCommandCursor) {
    return domainEventCursor;
  }
  if (!decodedDomainEventCursor) {
    return commandCursor;
  }

  return decodedDomainEventCursor.createdAt > decodedCommandCursor.createdAt
    ? domainEventCursor
    : commandCursor;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function readCommandPayloadDelta(row: TreeStreamCommandLogCursorRow): TreeStreamDeltaEvent | null {
  const commandName =
    typeof row.command_name === "string"
      ? row.command_name.trim()
      : typeof row.commandName === "string"
        ? row.commandName.trim()
        : "";
  if (commandName && TREE_STREAM_NOOP_COMMANDS.has(commandName)) {
    return {
      op: "noop",
    };
  }
  if (!isRecord(row.payload) || !("streamDelta" in row.payload)) {
    return null;
  }
  const candidate = row.payload.streamDelta;
  if (!isRecord(candidate) || typeof candidate.op !== "string") {
    return null;
  }

  return {
    op: candidate.op as TreeStreamDeltaEvent["op"],
    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent["node"]) : null,
    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent["document"]) : null,
    documentId: typeof candidate.documentId === "string" ? candidate.documentId : null,
    parentId: typeof candidate.parentId === "string" ? candidate.parentId : candidate.parentId === null ? null : undefined,
    sortOrder:
      typeof candidate.sortOrder === "number" && Number.isFinite(candidate.sortOrder)
        ? candidate.sortOrder
        : undefined,
    updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null,
    upsertDocuments: Array.isArray(candidate.upsertDocuments)
      ? (candidate.upsertDocuments as TreeStreamDeltaEvent["upsertDocuments"])
      : null,
    upsertAssets: Array.isArray(candidate.upsertAssets)
      ? (candidate.upsertAssets as TreeStreamDeltaEvent["upsertAssets"])
      : null,
    documents: Array.isArray(candidate.documents) ? (candidate.documents as TreeStreamDeltaEvent["documents"]) : null,
    sidebar: isRecord(candidate.sidebar) ? (candidate.sidebar as TreeStreamDeltaEvent["sidebar"]) : null,
  };
}

function readStreamDeltaCandidate(candidate: unknown): TreeStreamDeltaEvent | null {
  if (!isRecord(candidate) || typeof candidate.op !== "string") {
    return null;
  }

  return {
    op: candidate.op as TreeStreamDeltaEvent["op"],
    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent["node"]) : null,
    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent["document"]) : null,
    documentId: typeof candidate.documentId === "string" ? candidate.documentId : null,
    parentId: typeof candidate.parentId === "string" ? candidate.parentId : candidate.parentId === null ? null : undefined,
    sortOrder:
      typeof candidate.sortOrder === "number" && Number.isFinite(candidate.sortOrder)
        ? candidate.sortOrder
        : undefined,
    updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null,
    upsertDocuments: Array.isArray(candidate.upsertDocuments)
      ? (candidate.upsertDocuments as TreeStreamDeltaEvent["upsertDocuments"])
      : null,
    upsertAssets: Array.isArray(candidate.upsertAssets)
      ? (candidate.upsertAssets as TreeStreamDeltaEvent["upsertAssets"])
      : null,
    documents: Array.isArray(candidate.documents) ? (candidate.documents as TreeStreamDeltaEvent["documents"]) : null,
    sidebar: isRecord(candidate.sidebar) ? (candidate.sidebar as TreeStreamDeltaEvent["sidebar"]) : null,
  };
}

function readDomainEventPayloadDelta(row: TreeStreamDomainEventCursorRow): TreeStreamDeltaEvent | null {
  if (!isRecord(row.payload)) {
    return null;
  }
  return readStreamDeltaCandidate(row.payload.streamDelta ?? row.payload.stream_delta);
}

function readCommandRowCommandId(row: TreeStreamCommandLogCursorRow): string | null {
  const raw =
    typeof row.command_id === "string"
      ? row.command_id
      : typeof row.commandId === "string"
        ? row.commandId
        : typeof row.id === "string"
          ? row.id
          : "";
  const trimmed = raw.trim();
  return trimmed || null;
}

function readDomainEventCommandId(row: TreeStreamDomainEventCursorRow): string | null {
  const raw =
    typeof row.command_id === "string"
      ? row.command_id
      : typeof row.commandId === "string"
        ? row.commandId
        : "";
  const trimmed = raw.trim();
  return trimmed || null;
}

function resolveMatchingCommandDomainEventDelta(input: {
  commandRows: TreeStreamCommandLogCursorRow[];
  domainEventRows: TreeStreamDomainEventCursorRow[];
  commandDrifted: boolean;
  domainEventDrifted: boolean;
}): TreeStreamDeltaEvent | null {
  if (
    input.commandDrifted ||
    input.commandRows.length !== 1 ||
    input.domainEventRows.length !== 1
  ) {
    return null;
  }

  const commandId = readCommandRowCommandId(input.commandRows[0] ?? {});
  const eventCommandId = readDomainEventCommandId(input.domainEventRows[0] ?? {});
  if (!commandId || commandId !== eventCommandId) {
    return null;
  }

  const commandDelta = readCommandPayloadDelta(input.commandRows[0] ?? {});
  const eventDelta = readDomainEventPayloadDelta(input.domainEventRows[0] ?? {});
  if (!commandDelta || !eventDelta) {
    return null;
  }

  return JSON.stringify(commandDelta) === JSON.stringify(eventDelta) ? eventDelta : null;
}

function collectNewCommandLogs(input: {
  rows: TreeStreamCommandLogCursorRow[];
  previousCursor: string | null;
}) {
  const previousCursor = decodeTreeStreamCursor(input.previousCursor);
  if (!previousCursor) {
    return {
      rows: input.rows,
      drifted: false,
    };
  }

  const previousIndex = input.rows.findIndex((row) => {
    const id = typeof row.id === "string" ? row.id.trim() : "";
    const createdAt = typeof row.created_at === "string" ? row.created_at.trim() : "";
    return id === previousCursor.id && createdAt === previousCursor.createdAt;
  });

  if (previousIndex >= 0) {
    return {
      rows: input.rows.slice(0, previousIndex),
      drifted: false,
    };
  }

  const newerRows = input.rows.filter((row) => {
    const createdAt = typeof row.created_at === "string" ? row.created_at.trim() : "";
    return createdAt > previousCursor.createdAt;
  });
  if (newerRows.length < input.rows.length) {
    return {
      rows: newerRows,
      drifted: false,
    };
  }

  return {
    rows: input.rows,
    drifted: input.rows.length > 0,
  };
}

function collectNewDomainEvents(input: {
  rows: TreeStreamDomainEventCursorRow[];
  previousCursor: string | null;
}) {
  const previousCursor = decodeTreeStreamCursor(input.previousCursor);
  if (!previousCursor) {
    return {
      rows: input.rows,
      drifted: false,
    };
  }

  const previousId = previousCursor.id.startsWith("domain_event:")
    ? previousCursor.id.slice("domain_event:".length)
    : previousCursor.id;
  const previousIndex = input.rows.findIndex((row) => {
    const id =
      typeof row.event_id === "string"
        ? row.event_id.trim()
        : typeof row.id === "string"
          ? row.id.trim()
          : "";
    const createdAt =
      typeof row.created_at === "string"
        ? row.created_at.trim()
        : typeof row.createdAt === "string"
          ? row.createdAt.trim()
          : "";
    return id === previousId && createdAt === previousCursor.createdAt;
  });

  if (previousIndex >= 0) {
    return {
      rows: input.rows.slice(0, previousIndex),
      drifted: false,
    };
  }

  const newerRows = input.rows.filter((row) => {
    const createdAt =
      typeof row.created_at === "string"
        ? row.created_at.trim()
        : typeof row.createdAt === "string"
          ? row.createdAt.trim()
          : "";
    return createdAt > previousCursor.createdAt;
  });
  if (newerRows.length < input.rows.length) {
    return {
      rows: newerRows,
      drifted: false,
    };
  }

  return {
    rows: input.rows,
    drifted: input.rows.length > 0,
  };
}

function buildTreeStreamEnvelope(input: {
  kind: TreeStreamEventName;
  workspaceId: string;
  rootNodeId: string | null;
  projection: TreeStreamProjection;
  cursor: string | null;
  overview: TreeStreamOverview;
  snapshot: TreeStreamSnapshotPayload;
}): TreeStreamEnvelope {
  return {
    kind: input.kind,
    stream: input.rootNodeId ? "subtree" : "workspace",
    workspaceId: input.workspaceId,
    rootNodeId: input.rootNodeId,
    cursor: input.cursor,
    projection: input.projection,
    requestId: input.snapshot.requestId,
    traceId: input.snapshot.traceId,
    data: input.snapshot.data,
    snapshot: input.snapshot.snapshot,
    overview: input.overview,
  };
}

function buildTreeStreamDeltaEnvelope(input: {
  workspaceId: string;
  rootNodeId: string | null;
  projection: TreeStreamProjection;
  cursor: string | null;
  overview: TreeStreamOverview;
  snapshot: TreeStreamSnapshotPayload;
  delta: TreeStreamDeltaEvent;
}): TreeStreamEnvelope {
  return {
    kind: "delta",
    stream: input.rootNodeId ? "subtree" : "workspace",
    workspaceId: input.workspaceId,
    rootNodeId: input.rootNodeId,
    cursor: input.cursor,
    projection: input.projection,
    requestId: input.snapshot.requestId,
    traceId: input.snapshot.traceId,
    data: input.delta,
    snapshot: null,
    overview: input.overview,
  };
}

async function defaultSleep(ms: number) {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

export async function* streamTreeFrames(
  input: StreamTreeFramesInput,
): AsyncGenerator<TreeStreamFrame> {
  const contract = resolveTreeStreamContract({
    rootNodeId: input.rootNodeId,
  });
  const pollMs = Math.max(250, Math.floor(input.pollMs ?? 2000));
  const maxPolls =
    typeof input.maxPolls === "number" && Number.isFinite(input.maxPolls)
      ? Math.max(0, Math.floor(input.maxPolls))
      : null;
  const sleep = input.sleep ?? defaultSleep;

  let snapshot = await input.loadSnapshot();
  let overview = await input.loadOverview();
  let cursor = resolveOverviewCursor(overview, input.initialCursor ?? null);

  yield {
    event: "snapshot",
    payload: buildTreeStreamEnvelope({
      kind: "snapshot",
      workspaceId: input.workspaceId,
      rootNodeId: contract.rootNodeId,
      projection: contract.projection,
      cursor,
      overview,
      snapshot,
    }),
  };

  let polls = 0;
  while (maxPolls === null || polls < maxPolls) {
    polls += 1;
    await sleep(pollMs);

    overview = await input.loadOverview();
    const nextCursor = resolveOverviewCursor(overview, cursor);
    if (nextCursor === cursor) {
      continue;
    }

    const rows = Array.isArray(overview.command_logs) ? overview.command_logs : [];
    const newRows = collectNewCommandLogs({
      rows,
      previousCursor: cursor,
    });
    const eventRows = Array.isArray(overview.domain_events)
      ? (overview.domain_events as TreeStreamDomainEventCursorRow[])
      : [];
    const newEventRows = collectNewDomainEvents({
      rows: eventRows,
      previousCursor: cursor,
    });
    const hasNewCommandRows = newRows.rows.length > 0;
    const hasNewDomainEventRows = newEventRows.rows.length > 0;
    if (hasNewCommandRows && hasNewDomainEventRows) {
      const delta = resolveMatchingCommandDomainEventDelta({
        commandRows: newRows.rows,
        domainEventRows: newEventRows.rows,
        commandDrifted: newRows.drifted,
        domainEventDrifted: newEventRows.drifted,
      });
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,
            rootNodeId: contract.rootNodeId,
            projection: contract.projection,
            cursor,
            overview,
            snapshot,
            delta,
          }),
        };
        continue;
      }

      snapshot = await input.loadSnapshot();
      cursor = nextCursor;
      yield {
        event: "resync",
        payload: buildTreeStreamEnvelope({
          kind: "resync",
          workspaceId: input.workspaceId,
          rootNodeId: contract.rootNodeId,
          projection: contract.projection,
          cursor,
          overview,
          snapshot,
        }),
      };
      continue;
    }

    if (!newRows.drifted && newRows.rows.length === 1) {
      const delta = readCommandPayloadDelta(newRows.rows[0] ?? {});
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,
            rootNodeId: contract.rootNodeId,
            projection: contract.projection,
            cursor,
            overview,
            snapshot,
            delta,
          }),
        };
        continue;
      }
    }

    if (!newEventRows.drifted && newRows.rows.length === 0 && newEventRows.rows.length === 1) {
      const delta = readDomainEventPayloadDelta(newEventRows.rows[0] ?? {});
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,
            rootNodeId: contract.rootNodeId,
            projection: contract.projection,
            cursor,
            overview,
            snapshot,
            delta,
          }),
        };
        continue;
      }
    }

    snapshot = await input.loadSnapshot();
    cursor = nextCursor;

    yield {
      event: "resync",
      payload: buildTreeStreamEnvelope({
        kind: "resync",
        workspaceId: input.workspaceId,
        rootNodeId: contract.rootNodeId,
        projection: contract.projection,
        cursor,
        overview,
        snapshot,
      }),
    };
  }
}
