"""
Test scenario seeder for Festinger.

Seeds the database and in-memory cache with pre-defined world-model states,
then exposes a trigger endpoint so gutasktool (or a test script) can fire the
conflicting fact and observe the collision + resolution pipeline.

Scenarios
---------

A  misclassification
   Seed:    michigan ISPART usa  in dim:usa  (parent_id = dim_id — coarse/degenerate edge)
   Trigger: gutask iknowthat 'michigan -isa state in context of usa'
   Expect:  misclassification collision  →  resolution moves ISA fact to dim:type

B  ISA + ISA decomposition
   Seed:    gnommoweb ISA container  in dim:type
   Trigger: gutask iknowthat 'gnommoweb -isa repo in context of type'
   Expect:  isa_isa collision  →  nightly job decomposes into artifact-type + deployment-type

C  ISPART + ISPART contradiction
   Seed:    dobby ISPART docker_host_1  in dim:runs-on
   Trigger: gutask iknowthat 'dobby -ispart docker_host_2 in context of runs-on'
   Expect:  ispart_ispart collision  →  nightly job arbitrates (update or dismiss)
"""
from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any

import asyncpg

from . import cache
from .cache import SoasRow, UrdEdge
from .db import get_or_create_soas

log = logging.getLogger("festinger.test")


# ---------------------------------------------------------------------------
# Scenario definitions
# ---------------------------------------------------------------------------

@dataclass
class SeedEdge:
    concept: str
    parent: str
    dimension: str
    is_isa: bool
    confidence: float = 0.9
    source: str = "test"
    note: str = ""


@dataclass
class Scenario:
    id: str
    name: str
    description: str
    seed_edges: list[SeedEdge]
    trigger_fact: str          # the fact to POST to /iknowthat to cause the collision
    expected_collision: str    # 'isa_isa' | 'ispart_ispart' | 'misclassification'
    expected_resolution: str   # 'decompose' | 'update' | 'dismiss' | 'reclassify'
    notes: str = ""


SCENARIOS: dict[str, Scenario] = {

    "A": Scenario(
        id="A",
        name="misclassification — michigan/state/usa",
        description=(
            "The world model holds a coarse, degenerate fact: michigan ISPART usa "
            "in the 'usa' dimension (parent_id = dim_id). This is an early, "
            "undifferentiated encoding — the location context IS the dimension. "
            "A new fact 'michigan is a state in context of usa' is ISA in the same "
            "dimension, triggering a misclassification collision. Resolution should "
            "suggest moving the ISA fact to the 'type' dimension."
        ),
        seed_edges=[
            SeedEdge(
                concept="michigan",
                parent="usa",
                dimension="usa",        # parent_id = dim_id — the degenerate pattern
                is_isa=False,           # ISPART
                confidence=0.85,
                note="coarse pre-knowledge: michigan is part of usa, dim=usa (undifferentiated)",
            ),
        ],
        trigger_fact="michigan -isa state in context of usa",
        expected_collision="misclassification",
        expected_resolution="reclassify",
        notes=(
            "The degenerate parent_id=dim_id pattern arises naturally when the world model "
            "first hears 'michigan is in usa' before any geography dimension exists. "
            "Resolution should split: ISPART goes to 'geography', ISA goes to 'type'."
        ),
    ),

    "B": Scenario(
        id="B",
        name="ISA+ISA decomposition — gnommoweb/container/repo",
        description=(
            "The world model holds gnommoweb ISA container in dim:type. "
            "Both 'container' (deployment artifact) and 'repo' (software artifact) are "
            "simultaneously true, but the 'type' dimension is too coarse to hold both. "
            "Resolution should decompose into 'deployment-type' and 'artifact-type'."
        ),
        seed_edges=[
            SeedEdge(
                concept="gnommoweb",
                parent="container",
                dimension="type",
                is_isa=True,
                confidence=0.9,
                note="gnommoweb runs as a Docker container",
            ),
        ],
        trigger_fact="gnommoweb -isa repo in context of type",
        expected_collision="isa_isa",
        expected_resolution="decompose",
        notes="Classic dimension-too-coarse case from PROJECT.md test case B.",
    ),

    "C": Scenario(
        id="C",
        name="ISPART+ISPART contradiction — dobby/host migration",
        description=(
            "The world model holds dobby ISPART docker_host_1 in dim:runs-on. "
            "A new fact places dobby on docker_host_2, contradicting the existing "
            "runs-on edge. One host is correct; the nightly job must arbitrate."
        ),
        seed_edges=[
            SeedEdge(
                concept="dobby",
                parent="docker_host_1",
                dimension="runs-on",
                is_isa=False,
                confidence=0.9,
                note="dobby agent originally deployed on docker_host_1",
            ),
        ],
        trigger_fact="dobby -ispart docker_host_2 in context of runs-on",
        expected_collision="ispart_ispart",
        expected_resolution="update",
        notes="Simulates a container migration where the new host fact should win.",
    ),
}


# ---------------------------------------------------------------------------
# Seeder
# ---------------------------------------------------------------------------

async def seed_scenario(pool: asyncpg.Pool, scenario_id: str) -> dict[str, Any]:
    """
    Insert SOAS + URD entries for a named scenario.
    Returns a summary of what was created, including the token→id mapping
    and the trigger fact to POST to /iknowthat.
    """
    sc = SCENARIOS.get(scenario_id.upper())
    if sc is None:
        return {"error": f"unknown scenario: {scenario_id!r}. Valid: {list(SCENARIOS)}"}

    created_soas: list[dict] = []
    created_edges: list[dict] = []
    collisions: list[dict] = []

    for edge in sc.seed_edges:
        concept_row = await get_or_create_soas(pool, edge.concept)
        parent_row  = await get_or_create_soas(pool, edge.parent)
        dim_row     = await get_or_create_soas(pool, edge.dimension)

        for row in [concept_row, parent_row, dim_row]:
            created_soas.append({"token": row.token, "id": row.id})

        # Manually set encounter_count + saliency above read threshold so
        # the concept appears in recollections during the test prompt.
        async with pool.acquire() as conn:
            await conn.execute(
                "UPDATE soas SET encounter_count=50, saliency=1.5 WHERE id=$1",
                concept_row.id,
            )
        if concept_row.token in cache.soas_by_token:
            cache.soas_by_token[concept_row.token].encounter_count = 50
            cache.soas_by_token[concept_row.token].saliency = 1.5

        # Insert URD edge — direct Postgres write, bypassing collision detection,
        # because this is deliberate seed state (not a user-facing write path).
        key = (concept_row.id, dim_row.id)
        existing = cache.urd_by_concept_dim.get(key)

        if existing is not None:
            collisions.append({
                "concept": edge.concept,
                "dimension": edge.dimension,
                "message": "edge already present in cache — skipped",
            })
            continue

        try:
            async with pool.acquire() as conn:
                await conn.execute(
                    """
                    INSERT INTO urd (id, parent_id, dim_id, is_isa, confidence, source)
                    VALUES ($1, $2, $3, $4, $5, $6)
                    ON CONFLICT DO NOTHING
                    """,
                    concept_row.id, parent_row.id, dim_row.id,
                    edge.is_isa, edge.confidence, edge.source,
                )
        except Exception as e:
            collisions.append({"concept": edge.concept, "error": str(e)})
            continue

        # Update in-memory cache
        urd_edge = UrdEdge(
            concept_id=concept_row.id,
            parent_id=parent_row.id,
            dim_id=dim_row.id,
            is_isa=edge.is_isa,
            confidence=edge.confidence,
            source=edge.source,
            parent_token=parent_row.token,
            dim_token=dim_row.token,
        )
        cache.urd_by_concept.setdefault(concept_row.id, []).append(urd_edge)
        cache.urd_by_concept_dim[key] = urd_edge

        created_edges.append({
            "concept": edge.concept,
            "concept_id": concept_row.id,
            "parent": edge.parent,
            "parent_id": parent_row.id,
            "dimension": edge.dimension,
            "dim_id": dim_row.id,
            "is_isa": edge.is_isa,
            "parent_equals_dim": parent_row.id == dim_row.id,
            "note": edge.note,
        })

    return {
        "scenario": sc.id,
        "name": sc.name,
        "description": sc.description,
        "soas_entries": created_soas,
        "urd_edges": created_edges,
        "skipped_collisions": collisions,
        "next_step": {
            "action": "POST /iknowthat",
            "body": {"fact": sc.trigger_fact},
            "gutask_command": f"gutask iknowthat '{sc.trigger_fact}'",
            "expected_collision": sc.expected_collision,
            "expected_resolution": sc.expected_resolution,
        },
        "notes": sc.notes,
    }


async def reset_scenario(pool: asyncpg.Pool, scenario_id: str) -> dict[str, Any]:
    """
    Remove the URD edges and SOAS tokens introduced by a scenario seed.
    Clears the corresponding cache entries and resolution queue rows.
    Use before re-running a scenario to get a clean slate.
    """
    sc = SCENARIOS.get(scenario_id.upper())
    if sc is None:
        return {"error": f"unknown scenario: {scenario_id!r}"}

    removed_urd = 0
    removed_rq = 0

    concept_tokens = {e.concept for e in sc.seed_edges}
    concept_ids = [
        cache.soas_by_token[t].id
        for t in concept_tokens
        if t in cache.soas_by_token
    ]

    async with pool.acquire() as conn:
        for cid in concept_ids:
            # Remove resolution queue entries
            r = await conn.execute(
                "DELETE FROM resolution_queue WHERE concept_id=$1", cid
            )
            removed_rq += int(r.split()[-1])

            # Remove URD edges where this concept is the subject
            r = await conn.execute("DELETE FROM urd WHERE id=$1", cid)
            removed_urd += int(r.split()[-1])

            # Clear from in-memory cache
            cache.urd_by_concept.pop(cid, None)
            for key in [k for k in cache.urd_by_concept_dim if k[0] == cid]:
                del cache.urd_by_concept_dim[key]
            cache.pending_conflicts.discard(cid)

    return {
        "scenario": scenario_id.upper(),
        "removed_urd_edges": removed_urd,
        "removed_resolution_queue_rows": removed_rq,
        "status": "reset complete",
    }
