"""v19 — Re-classify historical claims stuck on category='misc'.

Before the category-classification fix landed (issue #227), claims that
the LLM returned with an unrecognised category label fell through to the
'misc' default.  Pre-v5 rows also received misc via ALTER TABLE DEFAULT.

This migration applies lightweight keyword heuristics to subject/predicate/
object_value fields to infer a better category for those stuck rows.
Only claims with HIGH-confidence signal are reclassified; ambiguous rows
stay as 'misc' to avoid false positives.

Heuristic priority order (first match wins):
  1. PREFERENCES  — stable like/dislike/usage predicates
  2. GOALS        — planning/target/milestone predicates
  3. ENGINEERING  — technical decision predicates + tech object keywords
  4. PERSONAL     — biographical/family/life predicates
  5. IDENTITY     — agent self-reflection predicates

The matching is case-insensitive via lower().

After updating category, intrinsic_type is also synced to stay consistent
with CATEGORY_TO_INTRINSIC_MAP.

Also exposes an async ``upgrade(conn, context)`` function that supports
``dry_run=True`` for testing and scripted use; the migration runner calls
``SQL_UP`` automatically.

Issue: #563
Milestone: v1.3.2 (#29)
"""
from __future__ import annotations

import logging
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

VERSION = 19
DESCRIPTION = "Re-classify historical misc-category claims via heuristics (#563)"

# ---------------------------------------------------------------------------
# SQL_UP — applied automatically by MigrationRunner
# ---------------------------------------------------------------------------

SQL_UP = """
-- Step 1: re-classify misc claims using keyword heuristics.
-- Matching is case-insensitive; first match wins (CASE short-circuits).
-- Only touches rows where category = 'misc' and the claim is not deleted.
UPDATE semantic_claim
SET category = CASE
    -- ----------------------------------------------------------------
    -- PREFERENCES: stable like/dislike/usage predicates
    -- ----------------------------------------------------------------
    WHEN lower(predicate) LIKE '%prefer%'
      OR lower(predicate) LIKE '%prefers%'
      OR lower(predicate) LIKE '%dislikes%'
      OR lower(predicate) LIKE '%likes%'
      OR lower(predicate) LIKE '%loves%'
      OR lower(predicate) LIKE '%hates%'
      OR lower(predicate) LIKE '%favors%'
      OR lower(predicate) LIKE '%avoids%'
      OR lower(predicate) LIKE '%enjoys%'
      OR lower(predicate) LIKE '%always uses%'
      OR lower(predicate) LIKE '%never uses%'
    THEN 'preferences'

    -- ----------------------------------------------------------------
    -- GOALS: planning/target/milestone predicates
    -- ----------------------------------------------------------------
    WHEN lower(predicate) LIKE '%plans to%'
      OR lower(predicate) LIKE '%aims to%'
      OR lower(predicate) LIKE '%targets%'
      OR lower(predicate) LIKE '%intends to%'
      OR lower(predicate) LIKE '%wants to achieve%'
      OR lower(predicate) LIKE '%hopes to%'
      OR lower(predicate) LIKE '%goal is%'
      OR lower(predicate) LIKE '%milestone%'
      OR lower(predicate) LIKE '%roadmap%'
    THEN 'goals'

    -- ----------------------------------------------------------------
    -- ENGINEERING: technical decision predicates OR tech-stack objects
    -- ----------------------------------------------------------------
    WHEN lower(predicate) LIKE '%decided to use%'
      OR lower(predicate) LIKE '%chose%'
      OR lower(predicate) LIKE '%switched to%'
      OR lower(predicate) LIKE '%implemented%'
      OR lower(predicate) LIKE '%deployed%'
      OR lower(predicate) LIKE '%discovered that%'
      OR lower(predicate) LIKE '%architecture%'
      OR lower(object_value) LIKE '%sqlite%'
      OR lower(object_value) LIKE '%postgres%'
      OR lower(object_value) LIKE '%postgresql%'
      OR lower(object_value) LIKE '%mysql%'
      OR lower(object_value) LIKE '%redis%'
      OR lower(object_value) LIKE '%docker%'
      OR lower(object_value) LIKE '%kubernetes%'
      OR lower(object_value) LIKE '%fly.io%'
      OR lower(object_value) LIKE '%migration runner%'
    THEN 'engineering'

    -- ----------------------------------------------------------------
    -- PERSONAL: biographical / family / life-event predicates
    -- ----------------------------------------------------------------
    WHEN lower(predicate) LIKE '%born in%'
      OR lower(predicate) LIKE '%grew up%'
      OR lower(predicate) LIKE '%lives in%'
      OR lower(predicate) LIKE '%is married%'
      OR lower(predicate) LIKE '%has sibling%'
      OR lower(predicate) LIKE '%has child%'
      OR lower(predicate) LIKE '%has daughter%'
      OR lower(predicate) LIKE '%has son%'
      OR lower(predicate) LIKE '%went to school%'
      OR lower(predicate) LIKE '%wakes up%'
      OR lower(predicate) LIKE '%works at%'
      OR lower(predicate) LIKE '%studies at%'
    THEN 'personal'

    -- ----------------------------------------------------------------
    -- IDENTITY: agent self-reflection (subject is agent-like)
    -- ----------------------------------------------------------------
    WHEN (
        lower(subject) LIKE '%eva%'
        OR lower(subject) LIKE '%agent%'
        OR lower(subject) LIKE '%ai%'
        OR lower(subject) LIKE '%assistant%'
    ) AND (
        lower(predicate) LIKE '%feel%'
        OR lower(predicate) LIKE '%feels%'
        OR lower(predicate) LIKE '%believes%'
        OR lower(predicate) LIKE '%identifies%'
        OR lower(predicate) LIKE '%experience%'
        OR lower(predicate) LIKE '%reflect%'
    )
    THEN 'identity'

    -- ----------------------------------------------------------------
    -- No strong signal — keep as 'misc'
    -- ----------------------------------------------------------------
    ELSE 'misc'
END
WHERE category = 'misc'
  AND (is_deleted = 0 OR is_deleted IS NULL);

-- Step 2: sync intrinsic_type to stay consistent with CATEGORY_TO_INTRINSIC_MAP.
-- Run for ALL rows whose intrinsic_type is inconsistent after the category update.
UPDATE semantic_claim
SET intrinsic_type = CASE category
    WHEN 'preferences' THEN 'preference'
    WHEN 'goals'       THEN 'goal'
    WHEN 'identity'    THEN 'fact'
    WHEN 'personal'    THEN 'fact'
    WHEN 'engineering' THEN 'fact'
    WHEN 'misc'        THEN 'fact'
    ELSE 'fact'
END
WHERE (is_deleted = 0 OR is_deleted IS NULL)
  AND intrinsic_type IS NOT NULL
  AND intrinsic_type != CASE category
    WHEN 'preferences' THEN 'preference'
    WHEN 'goals'       THEN 'goal'
    WHEN 'identity'    THEN 'fact'
    WHEN 'personal'    THEN 'fact'
    WHEN 'engineering' THEN 'fact'
    WHEN 'misc'        THEN 'fact'
    ELSE 'fact'
END;
"""

SQL_DOWN = """
-- Intentional no-op: reversing heuristic reclassification would require
-- storing the original category for each updated row, which is not
-- practical.  Restore from backup if a hard rollback is needed.
SELECT 1;
"""

# ---------------------------------------------------------------------------
# Python upgrade/downgrade — callable from scripts and tests (supports dry_run)
# ---------------------------------------------------------------------------

_HEURISTIC_CATEGORIES = ("preferences", "goals", "engineering", "personal", "identity")

_CLASSIFY_SQL = """
SELECT id, subject, predicate, object_value,
       CASE
           WHEN lower(predicate) LIKE '%prefer%'
             OR lower(predicate) LIKE '%prefers%'
             OR lower(predicate) LIKE '%dislikes%'
             OR lower(predicate) LIKE '%likes%'
             OR lower(predicate) LIKE '%loves%'
             OR lower(predicate) LIKE '%hates%'
             OR lower(predicate) LIKE '%favors%'
             OR lower(predicate) LIKE '%avoids%'
             OR lower(predicate) LIKE '%enjoys%'
             OR lower(predicate) LIKE '%always uses%'
             OR lower(predicate) LIKE '%never uses%'
           THEN 'preferences'

           WHEN lower(predicate) LIKE '%plans to%'
             OR lower(predicate) LIKE '%aims to%'
             OR lower(predicate) LIKE '%targets%'
             OR lower(predicate) LIKE '%intends to%'
             OR lower(predicate) LIKE '%wants to achieve%'
             OR lower(predicate) LIKE '%hopes to%'
             OR lower(predicate) LIKE '%goal is%'
             OR lower(predicate) LIKE '%milestone%'
             OR lower(predicate) LIKE '%roadmap%'
           THEN 'goals'

           WHEN lower(predicate) LIKE '%decided to use%'
             OR lower(predicate) LIKE '%chose%'
             OR lower(predicate) LIKE '%switched to%'
             OR lower(predicate) LIKE '%implemented%'
             OR lower(predicate) LIKE '%deployed%'
             OR lower(predicate) LIKE '%discovered that%'
             OR lower(predicate) LIKE '%architecture%'
             OR lower(object_value) LIKE '%sqlite%'
             OR lower(object_value) LIKE '%postgres%'
             OR lower(object_value) LIKE '%postgresql%'
             OR lower(object_value) LIKE '%mysql%'
             OR lower(object_value) LIKE '%redis%'
             OR lower(object_value) LIKE '%docker%'
             OR lower(object_value) LIKE '%kubernetes%'
             OR lower(object_value) LIKE '%fly.io%'
             OR lower(object_value) LIKE '%migration runner%'
           THEN 'engineering'

           WHEN lower(predicate) LIKE '%born in%'
             OR lower(predicate) LIKE '%grew up%'
             OR lower(predicate) LIKE '%lives in%'
             OR lower(predicate) LIKE '%is married%'
             OR lower(predicate) LIKE '%has sibling%'
             OR lower(predicate) LIKE '%has child%'
             OR lower(predicate) LIKE '%has daughter%'
             OR lower(predicate) LIKE '%has son%'
             OR lower(predicate) LIKE '%went to school%'
             OR lower(predicate) LIKE '%wakes up%'
             OR lower(predicate) LIKE '%works at%'
             OR lower(predicate) LIKE '%studies at%'
           THEN 'personal'

           WHEN (
               lower(subject) LIKE '%eva%'
               OR lower(subject) LIKE '%agent%'
               OR lower(subject) LIKE '%ai%'
               OR lower(subject) LIKE '%assistant%'
           ) AND (
               lower(predicate) LIKE '%feel%'
               OR lower(predicate) LIKE '%feels%'
               OR lower(predicate) LIKE '%believes%'
               OR lower(predicate) LIKE '%identifies%'
               OR lower(predicate) LIKE '%experience%'
               OR lower(predicate) LIKE '%reflect%'
           )
           THEN 'identity'

           ELSE 'misc'
       END AS new_category
FROM semantic_claim
WHERE category = 'misc'
  AND (is_deleted = 0 OR is_deleted IS NULL)
"""

_INTRINSIC_MAP = {
    "preferences": "preference",
    "goals": "goal",
    "identity": "fact",
    "personal": "fact",
    "engineering": "fact",
    "misc": "fact",
}


async def upgrade(conn, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    """Re-classify misc-category claims using keyword heuristics.

    Parameters
    ----------
    conn:
        Open aiosqlite connection.
    context:
        Optional dict.  Set ``context['dry_run'] = True`` to count affected
        rows without writing any changes.

    Returns
    -------
    dict with keys:
        ``total_misc``     — how many misc claims existed before the run
        ``reclassified``   — how many were assigned a non-misc category
        ``kept_misc``      — how many stayed misc (no strong signal)
        ``dry_run``        — whether dry_run mode was active
        ``breakdown``      — per-category reclassification counts
    """
    context = context or {}
    dry_run: bool = bool(context.get("dry_run", False))

    # Count total misc claims
    async with conn.execute(
        "SELECT COUNT(*) FROM semantic_claim WHERE category = 'misc'"
        " AND (is_deleted = 0 OR is_deleted IS NULL)"
    ) as cur:
        row = await cur.fetchone()
        total_misc: int = row[0] if row else 0

    if total_misc == 0:
        logger.info("v19 misc-category migration: no misc claims found — nothing to do.")
        return {
            "total_misc": 0,
            "reclassified": 0,
            "kept_misc": 0,
            "dry_run": dry_run,
            "breakdown": {},
        }

    # Collect what would change
    async with conn.execute(_CLASSIFY_SQL) as cur:
        rows = await cur.fetchall()

    reclassify_rows = [r for r in rows if r[4] != "misc"]  # (id, subj, pred, obj, new_cat)
    breakdown: Dict[str, int] = {}
    for row in reclassify_rows:
        cat = row[4]
        breakdown[cat] = breakdown.get(cat, 0) + 1

    reclassified = len(reclassify_rows)
    kept_misc = total_misc - reclassified

    if dry_run:
        logger.info(
            "v19 dry_run: total_misc=%d would_reclassify=%d kept_misc=%d breakdown=%s",
            total_misc, reclassified, kept_misc, breakdown,
        )
        return {
            "total_misc": total_misc,
            "reclassified": reclassified,
            "kept_misc": kept_misc,
            "dry_run": True,
            "breakdown": breakdown,
        }

    # Apply — run the same SQL as SQL_UP
    await conn.executescript(SQL_UP)
    await conn.commit()

    logger.info(
        "v19 applied: reclassified=%d kept_misc=%d breakdown=%s",
        reclassified, kept_misc, breakdown,
    )
    return {
        "total_misc": total_misc,
        "reclassified": reclassified,
        "kept_misc": kept_misc,
        "dry_run": False,
        "breakdown": breakdown,
    }


async def downgrade(conn) -> None:
    """No-op: see SQL_DOWN comment."""
    logger.warning("v19 downgrade is a no-op — restore from backup if needed.")
