"""Fact upsert — execute reconciliation decisions.

Ported from advisor_api memory_upsert.py (V2 section only).
Creates/versions MemoryFact records and generates embeddings via EmbeddingProvider.
"""

import logging
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from arandu.models import MemoryEntityRelationship, MemoryFact, MemoryFactEntityLink
from arandu.protocols import EmbeddingProvider
from arandu.write.entity_resolution import ResolvedEntity
from arandu.write.reconcile import ReconciliationDecision

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result types
# ---------------------------------------------------------------------------
@dataclass
class UpsertResult:
    """Result of executing reconciliation decisions."""

    added: int = 0
    updated: int = 0
    confirmed: list = field(default_factory=list)
    deleted: list = field(default_factory=list)
    created_facts: dict[str, list["MemoryFact"]] = field(default_factory=dict)


@dataclass
class RelationUpsertResult:
    """Result of upserting extracted relations."""

    created: int = 0


# ---------------------------------------------------------------------------
# Relation cascade helpers
# ---------------------------------------------------------------------------
async def _cascade_invalidate_relations(
    session: AsyncSession,
    fact_id: uuid.UUID,
    now: datetime,
) -> int:
    """Invalidate all active relations whose evidence_fact_id matches the given fact.

    Called after a fact is invalidated (UPDATE/DELETE) to propagate the
    invalidation to dependent relations.

    Args:
        session: Database session.
        fact_id: ID of the invalidated fact.
        now: Timestamp for invalidation.

    Returns:
        Number of relations invalidated.
    """
    stmt = select(MemoryEntityRelationship).where(
        MemoryEntityRelationship.evidence_fact_id == fact_id,
        MemoryEntityRelationship.invalidated_at.is_(None),
    )
    result = await session.execute(stmt)
    relations = list(result.scalars().all())
    for rel in relations:
        rel.valid_to = now
        rel.invalidated_at = now
        session.add(rel)
    if relations:
        logger.info(
            "cascade_invalidate: fact_id=%s, relations_invalidated=%d",
            fact_id,
            len(relations),
        )
    return len(relations)


def _match_relation_to_fact(
    source_entity: ResolvedEntity,
    target_entity: ResolvedEntity,
    created_facts: dict[str, list["MemoryFact"]],
) -> uuid.UUID | None:
    """Match a relation to a recently created fact using text heuristic.

    For a relation (source, target), searches ALL recently created facts
    (not just those keyed by source/target) for text mentioning either
    entity. With entity links, facts are already linked to all mentioned
    entities — the matcher just needs to find the right one.

    Returns the fact_id of the best match (highest confidence), or None.
    """
    candidates: list[tuple[uuid.UUID, float]] = []

    # Search ALL created facts, not just source/target keys.
    # With entity links, a fact about Clara mentioning Vertix is already
    # linked to both — we just need to find it by text.
    seen_ids: set[uuid.UUID] = set()
    unique_facts: list[MemoryFact] = []
    for fact_list in created_facts.values():
        for f in fact_list:
            fid = getattr(f, "id", None)
            if fid and fid not in seen_ids:
                seen_ids.add(fid)
                unique_facts.append(f)

    source_name = source_entity.display_name.lower()
    target_name = target_entity.display_name.lower()

    for fact in unique_facts:
        fact_text_lower = (getattr(fact, "fact_text", "") or "").lower()
        fact_id = getattr(fact, "id", None)
        confidence = getattr(fact, "confidence", 0.0) or 0.0
        if fact_id is None:
            continue

        # Strong match: both entity names appear in the fact text
        if source_name in fact_text_lower and target_name in fact_text_lower:
            candidates.append((fact_id, confidence + 1.0))  # boost strong matches
            continue

        # Weak match: fact belongs to one entity and mentions the OTHER
        fact_entity_key = getattr(fact, "entity_key", "") or ""
        if (fact_entity_key == source_entity.entity_key and target_name in fact_text_lower) or (
            fact_entity_key == target_entity.entity_key and source_name in fact_text_lower
        ):
            candidates.append((fact_id, confidence))
        elif fact_entity_key not in (source_entity.entity_key, target_entity.entity_key):
            # Fact from a third entity — check if it mentions both
            if source_name in fact_text_lower and target_name in fact_text_lower:
                candidates.append((fact_id, confidence))

    if not candidates:
        return None

    # Return the candidate with the highest confidence
    candidates.sort(key=lambda c: c[1], reverse=True)
    return candidates[0][0]


async def _create_mirror_fact(
    session: AsyncSession,
    user_id: str,
    source_event_id: str | None,
    source_entity: ResolvedEntity,
    target_entity: ResolvedEntity,
    rel_type: str,
    now: datetime,
    embeddings: EmbeddingProvider,
) -> MemoryFact:
    """Create a synthetic fact for a relation that has no corresponding extracted fact.

    Generates a human-readable fact_text from the relation components and
    persists it with low confidence (0.60) and source_context marking.

    Args:
        session: Database session.
        user_id: User identifier.
        source_event_id: ID of the source MemoryEvent.
        source_entity: Resolved source entity.
        target_entity: Resolved target entity.
        rel_type: Relationship type string.
        now: Timestamp for the fact.
        embeddings: Injected embedding provider.

    Returns:
        The newly created mirror MemoryFact (flushed, has ID).
    """
    rel_readable = rel_type.replace("_", " ")
    fact_text = f"{source_entity.display_name} {rel_readable} {target_entity.display_name}"
    embedding = await embeddings.embed_one(fact_text)

    fact = MemoryFact(
        user_id=user_id,
        entity_type=source_entity.entity_type,
        entity_key=source_entity.entity_key,
        entity_name=source_entity.display_name,
        fact_text=fact_text,
        confidence=0.60,
        importance=0.3,
        valid_from=now,
        source_event_id=source_event_id,
        source_context="inferred_from_relation",
        ingested_at=now,
        embedding_vec=embedding,
    )
    session.add(fact)
    await session.flush()
    return fact


# ---------------------------------------------------------------------------
# Decision executors
# ---------------------------------------------------------------------------
async def _add_fact(
    session: AsyncSession,
    user_id: str,
    source_event_id: str | None,
    decision: ReconciliationDecision,
    entity: ResolvedEntity,
    now: datetime,
    embeddings: EmbeddingProvider,
) -> MemoryFact:
    """Create a new MemoryFact from reconciliation decision.

    Returns:
        The newly created MemoryFact (flushed, has ID).
    """
    fact_text = f"{entity.display_name}: {decision.fact_text}"
    embedding = await embeddings.embed_one(fact_text)

    new_fact = MemoryFact(
        user_id=user_id,
        entity_type=entity.entity_type,
        entity_key=entity.entity_key,
        entity_name=entity.display_name,
        attribute_key=None,
        value_json=None,
        value_text=None,
        fact_text=decision.fact_text,
        confidence=decision.confidence,
        importance=0.5,
        valid_from=now,
        valid_to=None,
        source_event_id=source_event_id,
        last_confirmed_at=now,
        ingested_at=now,
        embedding_vec=embedding,
    )
    session.add(new_fact)
    await session.flush()
    return new_fact


async def _update_fact(
    session: AsyncSession,
    user_id: str,
    source_event_id: str | None,
    decision: ReconciliationDecision,
    entity: ResolvedEntity,
    now: datetime,
    embeddings: EmbeddingProvider,
) -> MemoryFact:
    """Close old fact, cascade-invalidate its relations, and create new fact.

    Returns:
        The newly created MemoryFact (flushed, has ID).
    """
    if decision.matched_fact_id:
        stmt = select(MemoryFact).where(MemoryFact.id == decision.matched_fact_id)
        result = await session.execute(stmt)
        old_fact = result.scalar_one_or_none()
        if old_fact:
            old_fact.valid_to = now
            old_fact.invalidated_at = now
            session.add(old_fact)

        # Cascade: invalidate relations that reference the old fact
        try:
            await _cascade_invalidate_relations(session, decision.matched_fact_id, now)
        except Exception as e:
            logger.warning(
                "cascade_invalidate: failed for fact_id=%s: %s",
                decision.matched_fact_id,
                e,
            )

    fact_text = f"{entity.display_name}: {decision.fact_text}"
    embedding = await embeddings.embed_one(fact_text)

    new_fact = MemoryFact(
        user_id=user_id,
        entity_type=entity.entity_type,
        entity_key=entity.entity_key,
        entity_name=entity.display_name,
        attribute_key=None,
        value_json=None,
        value_text=None,
        fact_text=decision.fact_text,
        confidence=decision.confidence,
        importance=0.5,
        valid_from=now,
        valid_to=None,
        source_event_id=source_event_id,
        supersedes_fact_id=decision.matched_fact_id,
        last_confirmed_at=now,
        ingested_at=now,
        embedding_vec=embedding,
    )
    session.add(new_fact)
    await session.flush()
    return new_fact


async def _confirm_fact(
    session: AsyncSession,
    decision: ReconciliationDecision,
    now: datetime,
) -> None:
    """Update last_confirmed_at on an existing fact (NOOP action)."""
    if decision.matched_fact_id:
        stmt = select(MemoryFact).where(MemoryFact.id == decision.matched_fact_id)
        result = await session.execute(stmt)
        fact = result.scalar_one_or_none()
        if fact:
            fact.last_confirmed_at = now
            session.add(fact)


async def _delete_fact(
    session: AsyncSession,
    decision: ReconciliationDecision,
    now: datetime,
) -> None:
    """Close a fact by setting valid_to and cascade-invalidate its relations."""
    if decision.matched_fact_id:
        stmt = select(MemoryFact).where(MemoryFact.id == decision.matched_fact_id)
        result = await session.execute(stmt)
        fact = result.scalar_one_or_none()
        if fact:
            fact.valid_to = now
            fact.invalidated_at = now
            session.add(fact)

        # Cascade: invalidate relations that reference the deleted fact
        try:
            await _cascade_invalidate_relations(session, decision.matched_fact_id, now)
        except Exception as e:
            logger.warning(
                "cascade_invalidate: failed for fact_id=%s: %s",
                decision.matched_fact_id,
                e,
            )


# ---------------------------------------------------------------------------
# Fact-Entity Links
# ---------------------------------------------------------------------------
async def _create_entity_links(
    session: AsyncSession,
    fact: MemoryFact,
    entity_map: dict[str, ResolvedEntity],
) -> None:
    """Create entity links for a persisted fact.

    Links the fact to: (a) its primary entity_key and (b) all entities
    from entity_map whose display_name appears in the fact text.
    Fail-safe: logs warning on error, never raises.
    """
    try:
        fact_text_lower = (fact.fact_text or "").lower()
        linked_keys: set[str] = set()

        # Primary link
        if fact.entity_key:
            link = MemoryFactEntityLink(
                fact_id=fact.id,
                entity_key=fact.entity_key,
                is_primary=True,
                user_id=fact.user_id,
            )
            session.add(link)
            linked_keys.add(fact.entity_key)

        # Secondary links: entities mentioned in the fact text
        for _name, resolved in entity_map.items():
            ek = resolved.entity_key
            if ek in linked_keys:
                continue
            display = (resolved.display_name or "").lower().strip()
            if len(display) < 3:
                continue  # skip very short names to avoid false positives
            if display in fact_text_lower:
                link = MemoryFactEntityLink(
                    fact_id=fact.id,
                    entity_key=ek,
                    is_primary=False,
                    user_id=fact.user_id,
                )
                session.add(link)
                linked_keys.add(ek)

        await session.flush()
    except Exception as e:
        logger.warning(
            "entity_links: failed to create links for fact %s: %s",
            fact.id,
            e,
        )


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------
async def execute_upsert(
    session: AsyncSession,
    user_id: str,
    source_event_id: str | None,
    decisions: list[ReconciliationDecision],
    entity_map: dict[str, ResolvedEntity],
    embeddings: EmbeddingProvider,
) -> UpsertResult:
    """Execute reconciliation decisions: ADD, UPDATE, NOOP, DELETE.

    Uses savepoint pattern for each decision to protect the outer transaction.

    Args:
        session: Database session.
        user_id: User identifier.
        source_event_id: ID of the source MemoryEvent.
        decisions: Reconciliation decisions to execute.
        entity_map: Resolved entities mapping.
        embeddings: Injected embedding provider.

    Returns:
        UpsertResult with counts.
    """
    result = UpsertResult()
    now = datetime.now(UTC)

    for decision in decisions:
        subj_key = decision.subject.lower().strip()
        entity = entity_map.get(subj_key)

        if entity is None:
            logger.warning(
                "execute_upsert: no entity for subject '%s', skipping",
                decision.subject,
            )
            continue

        try:
            if decision.action == "ADD":
                async with session.begin_nested():
                    new_fact = await _add_fact(
                        session,
                        user_id,
                        source_event_id,
                        decision,
                        entity,
                        now,
                        embeddings,
                    )
                await _create_entity_links(session, new_fact, entity_map)
                result.added += 1
                result.created_facts.setdefault(entity.entity_key, []).append(new_fact)

            elif decision.action == "UPDATE":
                async with session.begin_nested():
                    new_fact = await _update_fact(
                        session,
                        user_id,
                        source_event_id,
                        decision,
                        entity,
                        now,
                        embeddings,
                    )
                await _create_entity_links(session, new_fact, entity_map)
                result.updated += 1
                result.created_facts.setdefault(entity.entity_key, []).append(new_fact)

            elif decision.action == "NOOP":
                await _confirm_fact(session, decision, now)
                result.confirmed.append(
                    {
                        "fact_text": decision.fact_text,
                        "subject": decision.subject,
                        "matched_fact_id": str(decision.matched_fact_id) if decision.matched_fact_id else None,
                    }
                )

            elif decision.action == "DELETE":
                await _delete_fact(session, decision, now)
                result.deleted.append(
                    {
                        "fact_text": decision.fact_text,
                        "subject": decision.subject,
                        "matched_fact_id": str(decision.matched_fact_id) if decision.matched_fact_id else None,
                    }
                )
        except Exception as e:
            logger.warning(
                "execute_upsert: failed to execute %s for '%s': %s",
                decision.action,
                decision.subject,
                e,
            )

    logger.info(
        "execute_upsert: user=%s, added=%d, updated=%d, confirmed=%d, deleted=%d",
        user_id,
        result.added,
        result.updated,
        len(result.confirmed),
        len(result.deleted),
    )
    return result


# ---------------------------------------------------------------------------
# Entity alias registration
# ---------------------------------------------------------------------------
async def register_entity_alias(
    session: AsyncSession,
    user_id: str,
    canonical_key: str,
    alias: str,
    entity_type: str = "other",
) -> None:
    """Register an alias for an entity key using the MemoryEntityAlias table.

    Uses ON CONFLICT DO NOTHING for first-write-wins semantics.

    Args:
        session: Database session.
        user_id: User identifier.
        canonical_key: The canonical entity key.
        alias: The alias to register.
        entity_type: Entity type for the alias record.
    """
    from sqlalchemy.dialects.postgresql import insert as pg_insert

    from arandu.models import MemoryEntityAlias

    alias_normalized = alias.lower().strip()

    try:
        stmt = (
            pg_insert(MemoryEntityAlias)
            .values(
                user_id=user_id,
                alias=alias_normalized,
                canonical_entity_key=canonical_key,
                canonical_entity_type=entity_type,
            )
            .on_conflict_do_nothing()
        )
        await session.execute(stmt)
        logger.debug("register_entity_alias: key=%s, alias=%s", canonical_key, alias)
    except Exception as e:
        logger.debug("register_entity_alias: failed for key=%s alias=%s: %s", canonical_key, alias, e)


# ---------------------------------------------------------------------------
# Relation tracking
# ---------------------------------------------------------------------------
async def track_entity_relationship(
    session: AsyncSession,
    user_id: str,
    source_key: str,
    target_key: str,
    rel_type: str,
    *,
    strength: float = 0.5,
) -> None:
    """Create or reinforce a relationship between two entities.

    Args:
        session: Database session.
        user_id: User identifier.
        source_key: Source entity key.
        target_key: Target entity key.
        rel_type: Relationship type.
        strength: Initial/reinforcement strength.
    """
    stmt = select(MemoryEntityRelationship).where(
        MemoryEntityRelationship.user_id == user_id,
        MemoryEntityRelationship.source_entity_key == source_key,
        MemoryEntityRelationship.target_entity_key == target_key,
        MemoryEntityRelationship.rel_type == rel_type,
    )
    result = await session.execute(stmt)
    existing = result.scalar_one_or_none()

    if existing:
        existing.strength = min(1.0, (existing.strength or 0.5) + 0.05)
        existing.last_used_at = datetime.now(UTC)
        session.add(existing)
    else:
        rel = MemoryEntityRelationship(
            user_id=user_id,
            source_entity_key=source_key,
            target_entity_key=target_key,
            rel_type=rel_type,
            strength=strength,
            last_used_at=datetime.now(UTC),
        )
        session.add(rel)
        await session.flush()

    logger.debug(
        "track_entity_relationship: %s -> %s (%s)",
        source_key,
        target_key,
        rel_type,
    )


async def upsert_relations(
    session: AsyncSession,
    user_id: str,
    source_event_id: str | None,
    relations: Any,
    entity_map: dict[str, ResolvedEntity],
    created_facts: dict[str, list["MemoryFact"]] | None = None,
    embeddings: EmbeddingProvider | None = None,
) -> RelationUpsertResult:
    """Upsert relationships from extraction with evidence linkage.

    Resolves source/target via entity_map. Uses ON CONFLICT to deduplicate.
    When created_facts is provided, attempts to match each relation to a
    recently created fact and populate evidence_fact_id. When no match is
    found and embeddings is provided, creates a mirror fact as fallback.

    Args:
        session: Database session.
        user_id: User identifier.
        source_event_id: ID of the source MemoryEvent.
        relations: Extracted relations from LLM.
        entity_map: Resolved entities mapping.
        created_facts: Mapping of entity_key → list of recently created MemoryFact.
            Used for heuristic matching to populate evidence_fact_id.
        embeddings: Injected embedding provider. Required for creating mirror
            facts when no heuristic match is found.
    """
    from sqlalchemy import text
    from sqlalchemy.dialects.postgresql import insert as pg_insert

    result = RelationUpsertResult()
    _created_facts = created_facts or {}
    now = datetime.now(UTC)

    for rel in relations:
        source_key_lookup = rel.source.lower().strip()
        target_key_lookup = rel.target.lower().strip()

        source_entity = entity_map.get(source_key_lookup)
        target_entity = entity_map.get(target_key_lookup)

        if not source_entity or not target_entity:
            logger.debug(
                "upsert_relations: skipping rel %s->%s (unresolved entity)",
                rel.source,
                rel.target,
            )
            continue

        # Match relation to a recently created fact for evidence linkage
        evidence_fact_id: uuid.UUID | None = None
        if _created_facts:
            try:
                evidence_fact_id = _match_relation_to_fact(
                    source_entity,
                    target_entity,
                    _created_facts,
                )
            except Exception as e:
                logger.debug(
                    "upsert_relations: match failed for %s->%s: %s",
                    rel.source,
                    rel.target,
                    e,
                )

        # Fallback: create mirror fact for relations without evidence
        if evidence_fact_id is None and embeddings is not None:
            try:
                mirror_fact = await _create_mirror_fact(
                    session,
                    user_id,
                    source_event_id,
                    source_entity,
                    target_entity,
                    rel.rel_type,
                    now,
                    embeddings,
                )
                evidence_fact_id = mirror_fact.id
                logger.debug(
                    "upsert_relations: created mirror fact %s for %s->%s (%s)",
                    mirror_fact.id,
                    source_entity.entity_key,
                    target_entity.entity_key,
                    rel.rel_type,
                )
            except Exception as e:
                logger.warning(
                    "upsert_relations: mirror fact creation failed for %s->%s: %s",
                    rel.source,
                    rel.target,
                    e,
                )

        try:
            values: dict[str, Any] = {
                "user_id": user_id,
                "source_entity_key": source_entity.entity_key,
                "target_entity_key": target_entity.entity_key,
                "rel_type": rel.rel_type,
                "strength": 0.8,
                "provenance": "llm",
                "valid_from": datetime.now(UTC),
            }
            if evidence_fact_id is not None:
                values["evidence_fact_id"] = evidence_fact_id

            async with session.begin_nested():
                stmt = (
                    pg_insert(MemoryEntityRelationship)
                    .values(**values)
                    .on_conflict_do_update(
                        constraint="uq_entity_relationship",
                        set_={
                            "strength": text("LEAST(1.0, memory_entity_relationships.strength + 0.1)"),
                            "updated_at": datetime.now(UTC),
                            **({"evidence_fact_id": evidence_fact_id} if evidence_fact_id is not None else {}),
                        },
                    )
                )
                await session.execute(stmt)
            result.created += 1

            if evidence_fact_id:
                logger.debug(
                    "upsert_relations: %s->%s linked to fact %s",
                    source_entity.entity_key,
                    target_entity.entity_key,
                    evidence_fact_id,
                )
        except Exception as e:
            logger.warning(
                "upsert_relations: failed %s->%s (%s): %s",
                rel.source,
                rel.target,
                rel.rel_type,
                e,
            )

    if relations:
        logger.info(
            "upsert_relations: user=%s, processed=%d, created=%d",
            user_id,
            len(relations),
            result.created,
        )
    return result
