"""Latency comparison: pipeline path vs legacy path.

Runs both paths with identical workloads and asserts:
1. Pipeline framework adds less than 5ms overhead per write
2. Both paths write identical memory fields to the DB
3. All writes actually land in PostgreSQL

Requires a running PostgreSQL instance (integration test).
"""

import statistics
import time
import uuid

import pytest
from sqlalchemy import func, select

from common.models.memory import Memory
from core_api.schemas import MemoryCreate, MemoryOut

ITERATIONS = 10

# Two separate tenants so both paths can write the same content
# without triggering cross-path dedup
LEGACY_TENANT = f"test-lat-legacy-{uuid.uuid4().hex[:8]}"
PIPELINE_TENANT = f"test-lat-pipe-{uuid.uuid4().hex[:8]}"
FLEET_ID = "test-fleet"
AGENT_ID = "test-agent"


def _make_content(idx: int) -> str:
    """Deterministic per-index content. Same idx → same content for both paths."""
    # Use a fixed seed per index so content is reproducible but unique across indices
    # 4 "words" derived from index to give fake_embedding enough differentiation
    words = [f"word{idx * 4 + j}xyz{idx}" for j in range(4)]
    return (
        f"Memory number {idx} about {' '.join(words)} — "
        f"this content exercises the full write path for latency benchmarking."
    )


def _make_input(tenant_id: str, idx: int, **kwargs) -> MemoryCreate:
    return MemoryCreate(
        tenant_id=tenant_id,
        fleet_id=FLEET_ID,
        agent_id=AGENT_ID,
        content=_make_content(idx),
        persist=True,
        entity_links=[],
        **kwargs,
    )


# ── Fields that must be identical between legacy and pipeline rows ──
_COMPARED_FIELDS = [
    "tenant_id",
    "fleet_id",
    "agent_id",
    "memory_type",
    "title",
    "content",
    "weight",
    "source_uri",
    "run_id",
    "content_hash",
    "subject_entity_id",
    "predicate",
    "object_value",
    "ts_valid_start",
    "ts_valid_end",
    "status",
    "visibility",
]

# ── Metadata keys that must match (excluding timing/latency fields) ──
_COMPARED_META_KEYS = [
    "summary",
    "tags",
    "contains_pii",
    "pii_types",
    "embedding_pending",
]
# Timing-dependent metadata (both paths should populate, but values differ):
# write_latency_ms, semantic_dedup_ms, llm_ms


@pytest.mark.asyncio
async def test_pipeline_overhead_under_5ms(db):
    """Pipeline path adds <5ms overhead vs legacy, writes equivalent rows."""
    import logging

    from core_api.services import memory_service
    from core_api.services.memory_service import create_memory

    # Suppress the runner's per-step INFO lines for the duration of this test.
    # The runner now emits structured ``extra={...}`` dicts (CAURA-602) that
    # legitimately cost ~200us per step — measurable, but orthogonal to what
    # this test is guarding (pipeline-framework glue overhead). The runner's
    # ``isEnabledFor(INFO)`` guard skips the dict build when INFO is filtered.
    runner_logger = logging.getLogger("core_api.pipeline.runner")
    prev_level = runner_logger.level
    runner_logger.setLevel(logging.WARNING)

    original = memory_service._USE_PIPELINE_WRITE

    try:
        # ── Warmup (prime imports, caches, connections) ──
        memory_service._USE_PIPELINE_WRITE = False
        await create_memory(
            db,
            MemoryCreate(
                tenant_id=LEGACY_TENANT,
                fleet_id=FLEET_ID,
                agent_id=AGENT_ID,
                content=f"warmup {uuid.uuid4().hex} — enough content for the quality gate check.",
                persist=True,
                entity_links=[],
            ),
        )
        memory_service._USE_PIPELINE_WRITE = True
        await create_memory(
            db,
            MemoryCreate(
                tenant_id=PIPELINE_TENANT,
                fleet_id=FLEET_ID,
                agent_id=AGENT_ID,
                content=f"warmup {uuid.uuid4().hex} — enough content for the quality gate check.",
                persist=True,
                entity_links=[],
            ),
        )

        # ── Measure legacy path ──
        memory_service._USE_PIPELINE_WRITE = False
        legacy_latencies = []
        for i in range(ITERATIONS):
            data = _make_input(LEGACY_TENANT, i)
            t0 = time.perf_counter()
            result = await create_memory(db, data)
            legacy_latencies.append((time.perf_counter() - t0) * 1000)
            assert isinstance(result, MemoryOut)

        # ── Measure pipeline path (same content, different tenant) ──
        # Force strong mode to match legacy path behavior (both run semantic dedup)
        memory_service._USE_PIPELINE_WRITE = True
        pipeline_latencies = []
        for i in range(ITERATIONS):
            data = _make_input(PIPELINE_TENANT, i, write_mode="strong")
            t0 = time.perf_counter()
            result = await create_memory(db, data)
            pipeline_latencies.append((time.perf_counter() - t0) * 1000)
            assert isinstance(result, MemoryOut)

        # ── DB row count verification ──
        for tenant, label in [(LEGACY_TENANT, "legacy"), (PIPELINE_TENANT, "pipeline")]:
            count = (
                await db.execute(
                    select(func.count())
                    .select_from(Memory)
                    .where(Memory.tenant_id == tenant)
                )
            ).scalar()
            expected = 1 + ITERATIONS  # 1 warmup + ITERATIONS
            print(f"\n{label}: {count} rows in DB (expected {expected})")
            assert count == expected, f"{label}: expected {expected} rows, got {count}"

        # ── Field-by-field comparison of DB rows ──
        # Query the ITERATIONS memories (exclude warmup) ordered by content
        # so index i matches between tenants
        legacy_rows = (
            (
                await db.execute(
                    select(Memory)
                    .where(
                        Memory.tenant_id == LEGACY_TENANT,
                        Memory.content.like("Memory number %"),
                    )
                    .order_by(Memory.content)
                )
            )
            .scalars()
            .all()
        )

        pipeline_rows = (
            (
                await db.execute(
                    select(Memory)
                    .where(
                        Memory.tenant_id == PIPELINE_TENANT,
                        Memory.content.like("Memory number %"),
                    )
                    .order_by(Memory.content)
                )
            )
            .scalars()
            .all()
        )

        assert len(legacy_rows) == ITERATIONS, f"legacy: got {len(legacy_rows)} rows"
        assert len(pipeline_rows) == ITERATIONS, (
            f"pipeline: got {len(pipeline_rows)} rows"
        )

        mismatches = []
        for i, (leg, pip) in enumerate(zip(legacy_rows, pipeline_rows)):
            for field in _COMPARED_FIELDS:
                leg_val = getattr(leg, field)
                pip_val = getattr(pip, field)
                # tenant_id intentionally differs
                if field == "tenant_id":
                    continue
                # content_hash differs because tenant_id is part of the hash
                if field == "content_hash":
                    # Both should be non-None
                    assert leg_val is not None, f"row {i}: legacy content_hash is None"
                    assert pip_val is not None, (
                        f"row {i}: pipeline content_hash is None"
                    )
                    continue
                if leg_val != pip_val:
                    mismatches.append(
                        f"row {i} field '{field}': legacy={leg_val!r} vs pipeline={pip_val!r}"
                    )

            # Compare embeddings: both should be non-None and identical
            # (same content → same fake_embedding output)
            assert leg.embedding is not None, f"row {i}: legacy embedding is None"
            assert pip.embedding is not None, f"row {i}: pipeline embedding is None"
            assert list(leg.embedding) == list(pip.embedding), (
                f"row {i}: embeddings differ"
            )

            # Compare metadata (excluding timing fields)
            leg_meta = leg.metadata_ or {}
            pip_meta = pip.metadata_ or {}
            for key in _COMPARED_META_KEYS:
                if leg_meta.get(key) != pip_meta.get(key):
                    mismatches.append(
                        f"row {i} metadata['{key}']: "
                        f"legacy={leg_meta.get(key)!r} vs pipeline={pip_meta.get(key)!r}"
                    )

            # Timing metadata: values differ but both paths must populate them
            for timing_key in ["semantic_dedup_ms", "write_latency_ms"]:
                leg_has = timing_key in leg_meta
                pip_has = timing_key in pip_meta
                if leg_has != pip_has:
                    mismatches.append(
                        f"row {i} metadata['{timing_key}'] presence: "
                        f"legacy={leg_has} vs pipeline={pip_has}"
                    )

        if mismatches:
            print(f"\nFIELD MISMATCHES ({len(mismatches)}):")
            for m in mismatches:
                print(f"  - {m}")
        assert not mismatches, f"{len(mismatches)} field mismatches found"

        print(
            f"\nField comparison: {ITERATIONS} row pairs × {len(_COMPARED_FIELDS)} fields — ALL MATCH"
        )

        # ── Latency report ──
        legacy_median = statistics.median(legacy_latencies)
        pipeline_median = statistics.median(pipeline_latencies)
        overhead = pipeline_median - legacy_median

        print(f"\n{'=' * 60}")
        print(f"LATENCY COMPARISON ({ITERATIONS} iterations each)")
        print(f"{'=' * 60}")
        print(
            f"Legacy   — median: {legacy_median:.1f}ms  "
            f"p95: {_percentile(legacy_latencies, 95):.1f}ms  "
            f"min: {min(legacy_latencies):.1f}ms  "
            f"max: {max(legacy_latencies):.1f}ms"
        )
        print(
            f"Pipeline — median: {pipeline_median:.1f}ms  "
            f"p95: {_percentile(pipeline_latencies, 95):.1f}ms  "
            f"min: {min(pipeline_latencies):.1f}ms  "
            f"max: {max(pipeline_latencies):.1f}ms"
        )
        print(f"Overhead — {overhead:+.1f}ms (median)")
        print(f"{'=' * 60}")

        assert overhead < 5.0, (
            f"Pipeline overhead {overhead:.1f}ms exceeds 5ms budget. "
            f"Legacy median={legacy_median:.1f}ms, "
            f"Pipeline median={pipeline_median:.1f}ms"
        )

    finally:
        memory_service._USE_PIPELINE_WRITE = original
        runner_logger.setLevel(prev_level)


def _percentile(data: list[float], pct: float) -> float:
    """Simple percentile calculation."""
    sorted_data = sorted(data)
    idx = (pct / 100) * (len(sorted_data) - 1)
    lower = int(idx)
    upper = lower + 1
    if upper >= len(sorted_data):
        return sorted_data[-1]
    frac = idx - lower
    return sorted_data[lower] * (1 - frac) + sorted_data[upper] * frac
