#!/usr/bin/env python3
"""
Update rules database from authoritative sources:
  1. Skills  → database/deathwatch_skills_p94_107.csv
  2. Talents → database/rules/CR.txt  (Chapter IV, lines ~7272-8431)
  3. Delete garbled advance-table entries

Uses the REST API at http://localhost:5000
"""

import csv, json, re, sys, urllib.request, urllib.error
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
API  = "http://localhost:5000/api"
GM_SECRET = "bongo"

# ── helpers ───────────────────────────────────────────────────────────────────

def api_call(method, path, body=None):
    url = API + path
    data = json.dumps(body).encode() if body else None
    headers = {"Content-Type": "application/json", "x-gm-secret": GM_SECRET}
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=15) as r:
            return json.loads(r.read())
    except urllib.error.HTTPError as e:
        print(f"  HTTP {e.code} {method} {path}: {e.read().decode()[:200]}")
        return None

def upsert_rule(rule_id, title, content, page, source, source_abbr, category):
    """Insert or replace a rule in the DB via direct SQL through a helper endpoint.
    Falls back to delete+insert since the API doesn't expose a PATCH endpoint."""
    import mysql.connector
    # We'll do this via mysql directly (faster than HTTP for bulk)
    return (rule_id, title, content, page, source, source_abbr, category)

# ── 1. Skills from CSV ─────────────────────────────────────────────────────────

def load_skills():
    csvpath = REPO / "database" / "deathwatch_skills_p94_107.csv"
    with open(csvpath, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))

def build_skill_content(row):
    parts = []
    if row.get("skill_text"):
        parts.append(row["skill_text"].strip())
    if row.get("skill_description"):
        parts.append(row["skill_description"].strip())
    if row.get("skill_use"):
        parts.append(f"Use: {row['skill_use'].strip()}")
    return "\n\n".join(parts)

# ── 2. Talents from CR.txt ─────────────────────────────────────────────────────

# Known talent names (from Table 4-1, p108-111 of Core Rulebook)
KNOWN_TALENTS = {
    "abhor the witch", "air of authority", "ambidextrous",
    "assassin strike", "astartes weapon specialisation",
    "battle rage", "berserk charge", "binary chatter",
    "blademaster", "blind fighting", "bulging biceps",
    "catfall", "combat formation", "combat master",
    "crack shot", "crushing blow", "deadeye shot",
    "die hard", "disturbing voice", "double team",
    "dual shot", "dual strike", "enemy", "eye of vengeance",
    "fast hands", "fearless", "foresight",
    "gunslinger", "hard target", "hatred",
    "heightened senses", "hip shooting", "horde fighter",
    "hunter of aliens", "independent targeting",
    "inspire wrath", "into the jaws of hell",
    "iron affinity", "iron discipline", "iron jaw",
    "jaded", "killing strike", "leap up",
    "leaping dodge", "light sleeper", "lightning attack",
    "lightning reflexes", "litany of hate", "logis implant",
    "luminen blast", "luminen charge", "luminen shock",
    "maglev grace", "marksman", "master craftsman",
    "mechadendrite use", "meditation", "mental rage",
    "mighty shot", "nerves of steel", "night vision",
    "peer", "pistol training", "precise blow",
    "psy rating", "quick draw", "rapid reaction",
    "rapid reload", "resistance", "rite of fear",
    "rite of pure thought", "sacrificial strike",
    "sharpshooter", "slayer of daemons", "sound constitution",
    "sprint", "step aside", "storm of iron",
    "strong minded", "swift attack", "talent",
    "thunder charge", "true grit", "two weapon wielder",
    "unarmed master", "unarmed warrior",
    "unnatural characteristic", "warp affinity",
    "weapon training",
    # Chapter speciality talents
    "astartes assault", "astartes devastator", "astartes librarian",
    "astartes tactical", "astartes techmarine",
    "basic weapon training", "bolt weapon mastery",
    "energy cache", "enhanced bionic frame",
    "frenzy", "furious assault", "flesh render",
    "grenade mastery", "hammer blow", "hard target",
    "head shot", "incendiary charge", "mechadendrite use",
    "overcharge", "overwatch", "precision killer",
    "stay vigilant", "tactical advance",
}

def normalise_talent_name(raw):
    """Normalise OCR talent name to lowercase clean string."""
    # Remove OCR random-cap artifacts: if string is mixed case with no obvious pattern,
    # normalise to lowercase
    return re.sub(r'\s+', ' ', raw).strip().lower()

def split_two_columns(line):
    """Split a line from a two-column PDF layout.
    Returns (left, right) stripped strings, either may be empty."""
    # Find the largest run of whitespace (gap between columns)
    matches = list(re.finditer(r' {5,}', line))
    if not matches:
        return line.strip(), ""
    # Pick the widest gap; if there are multiple, prefer the one closest to centre
    best = max(matches, key=lambda m: m.end() - m.start())
    left  = line[:best.start()].strip()
    right = line[best.end():].strip()
    return left, right

def looks_like_talent_name(s):
    """Heuristic: a talent name is a short phrase (≤8 words) with no digits/punctuation."""
    s = s.strip()
    if not s or len(s) > 70 or len(s) < 3:
        return False
    if re.search(r'[0-9:;|]', s):
        return False
    words = s.split()
    if len(words) > 8:
        return False
    # Must be mostly letters
    letters = sum(c.isalpha() for c in s)
    if letters / len(s) < 0.7:
        return False
    norm = normalise_talent_name(s)
    # Must match a known talent or be a plausible name
    for kt in KNOWN_TALENTS:
        if kt == norm or kt.startswith(norm) or norm.startswith(kt):
            return True
    # fallback: allow if it looks like a heading (no verb, short)
    return False

def parse_talents_from_cr(cr_path):
    """Parse the talent descriptions from CR.txt using column-splitting."""
    with open(cr_path, encoding="utf-8") as f:
        lines = f.readlines()

    # Find start of talent descriptions (after the talent table)
    START_HINT = 7272   # from analysis
    END_HINT   = 8450

    relevant = lines[START_HINT - 1 : END_HINT]

    # Rebuild two separate column streams
    left_stream  = []
    right_stream = []

    for line in relevant:
        line = line.rstrip('\n')
        l, r = split_two_columns(line)
        left_stream.append(l)
        right_stream.append(r)

    def extract_talents_from_stream(stream):
        """Extract {name, prerequisites, description} dicts from one column stream."""
        talents = []
        current = None
        prereq_done = False

        for line in stream:
            line = line.strip()
            if not line:
                continue

            # Check if this is a "Prerequisites:" line
            pm = re.match(r'^Prerequisites?:\s*(.*)', line, re.IGNORECASE)
            if pm and current is not None:
                current['prerequisites'] = pm.group(1).strip()
                prereq_done = True
                continue

            # Check if it looks like a talent name
            if looks_like_talent_name(line) and (current is None or prereq_done):
                if current and current.get('description'):
                    talents.append(current)
                current = {'name': line, 'prerequisites': '—', 'description': ''}
                prereq_done = False
                continue

            # Otherwise it's description text
            if current is not None:
                if current['description']:
                    current['description'] += ' ' + line
                else:
                    current['description'] = line

        if current and current.get('description'):
            talents.append(current)

        return talents

    left_talents  = extract_talents_from_stream(left_stream)
    right_talents = extract_talents_from_stream(right_stream)

    # Merge and deduplicate by normalised name
    all_talents = {}
    for t in left_talents + right_talents:
        norm = normalise_talent_name(t['name'])
        if norm not in all_talents or len(t['description']) > len(all_talents[norm]['description']):
            all_talents[norm] = t

    return list(all_talents.values())

# ── 3. Direct DB update via mysql.connector ────────────────────────────────────

def get_db():
    import mysql.connector
    import os
    def load_env_file(path):
        if not path.exists():
            return
        for line in path.read_text().splitlines():
            line = line.strip()
            if not line or line.startswith('#') or '=' not in line:
                continue
            key, value = line.split('=', 1)
            os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
    load_env_file(REPO / 'database' / '.env')
    load_env_file(REPO / '.env')
    if not os.environ.get('DB_PASSWORD'):
        raise RuntimeError('DB_PASSWORD must be set in database/.env or .env')
    return mysql.connector.connect(
        host=os.environ.get('DB_HOST', '192.168.1.113'),
        port=int(os.environ.get('DB_PORT', '3307')),
        user=os.environ.get('DB_USER', 'deathwatch'),
        password=os.environ['DB_PASSWORD'],
        database=os.environ.get('DB_NAME', 'deathwatch'),
    )

def run():
    print("=== Deathwatch Rules Updater ===\n")

    # Try direct DB connection
    try:
        db = get_db()
        cursor = db.cursor()
        print("Connected to MariaDB directly.\n")
    except Exception as e:
        print(f"Cannot connect to MariaDB: {e}")
        print("Falling back to HTTP API (slower).")
        db = None
        cursor = None

    def exec_sql(sql, params=()):
        if cursor:
            cursor.execute(sql, params)
        else:
            print(f"  [no-db] would execute: {sql[:80]}")

    def upsert(rule_id, title, content, page, source, source_abbr, category):
        if cursor:
            cursor.execute(
                """INSERT INTO rules (rule_id, title, content, page, source, source_abbr, category)
                   VALUES (%s, %s, %s, %s, %s, %s, %s)
                   ON DUPLICATE KEY UPDATE
                     title=VALUES(title), content=VALUES(content),
                     page=VALUES(page), source=VALUES(source),
                     source_abbr=VALUES(source_abbr), category=VALUES(category)""",
                (rule_id, title, content, page, source, source_abbr, category)
            )
        else:
            print(f"  [no-db] upsert '{title}'")

    # ── Step 1: Delete garbage advance-table entries ──────────────────────────
    print("Step 1: Removing garbled advance-table entries...")
    garbage_patterns = [
        # Advance tables masquerading as rules
        "title LIKE 'AdvanceCostType%'",
        # Rules whose entire content is a cost/prerequisites table
        "content REGEXP '^[A-Za-z ]+[[:space:]]+[0-9,]+[[:space:]]+(Skill|Talent)[[:space:]]'",
        # Very short meaningless entries (under 30 chars content, sanitized source)
        "(source = 'sanitized' AND LENGTH(content) < 40)",
    ]
    deleted = 0
    for pat in garbage_patterns:
        if cursor:
            cursor.execute(f"SELECT COUNT(*) FROM rules WHERE {pat}")
            count = cursor.fetchone()[0]
            cursor.execute(f"DELETE FROM rules WHERE {pat}")
            print(f"  Deleted {count} rows matching: {pat[:60]}")
            deleted += count
        else:
            print(f"  [no-db] would delete: {pat[:60]}")
    print(f"  Total deleted: {deleted}\n")

    # ── Step 2: Skills from CSV ───────────────────────────────────────────────
    print("Step 2: Upserting skills from CSV...")
    skills = load_skills()
    for skill in skills:
        name    = skill['name'].strip()
        content = build_skill_content(skill)
        char    = skill.get('characteristic', '').strip()
        stype   = skill.get('type', '').strip()
        desc_header = f"{stype} Skill – {char}" if char else stype
        full_content = f"{desc_header}\n\n{content}" if desc_header else content

        rule_id = "skill_" + re.sub(r'[^a-z0-9]', '_', name.lower())
        upsert(rule_id, name, full_content, None,
               "Core Rulebook", "CR", "skills")
        print(f"  ✓ {name}")
    if db:
        db.commit()
    print(f"  {len(skills)} skills updated.\n")

    # ── Step 3: Talents from CR.txt ───────────────────────────────────────────
    print("Step 3: Parsing and upserting talents from CR.txt...")
    cr_path = REPO / "database" / "rules" / "CR.txt"
    talents = parse_talents_from_cr(cr_path)
    print(f"  Parsed {len(talents)} talent entries from CR.txt")

    imported = 0
    for t in talents:
        name = re.sub(r'\s+', ' ', t['name']).strip()
        # Clean up OCR capitalisation: convert random-caps to title case
        name_clean = re.sub(r'([A-Z])', lambda m: m.group(1).lower(), name)
        name_clean = ' '.join(w.capitalize() for w in name_clean.split())

        prereq = t.get('prerequisites', '—').strip()
        desc   = t.get('description', '').strip()
        if len(desc) < 20:
            continue  # Skip empty/garbage entries

        content = f"Prerequisites: {prereq}\n\n{desc}"
        rule_id = "talent_" + re.sub(r'[^a-z0-9]', '_', name_clean.lower())

        upsert(rule_id, name_clean, content, None,
               "Core Rulebook", "CR", "talents")
        imported += 1

    if db:
        db.commit()
    print(f"  {imported} talents upserted.\n")

    # ── Step 4: Fix existing sanitized rules – remove pure table entries ──────
    print("Step 4: Cleaning up remaining garbled sanitized entries...")
    if cursor:
        # Rules where the title IS the first line of the content (typical for advance tables)
        cursor.execute("""
            DELETE FROM rules
            WHERE source = 'sanitized'
              AND (
                content LIKE '%Cost%Type%Prerequisites%'
                OR content REGEXP '[0-9]{3,}[[:space:]]+(Skill|Talent)[[:space:]]'
                OR (title = content)
              )
        """)
        n = cursor.rowcount
        db.commit()
        print(f"  Deleted {n} more garbled entries.\n")
    else:
        print("  [no-db] skipped.\n")

    # ── Done ──────────────────────────────────────────────────────────────────
    if cursor:
        cursor.execute("SELECT COUNT(*) FROM rules")
        total = cursor.fetchone()[0]
        cursor.execute("SELECT source, COUNT(*) FROM rules GROUP BY source ORDER BY COUNT(*) DESC")
        by_source = cursor.fetchall()
        print(f"=== Done. Total rules in DB: {total} ===")
        for src, cnt in by_source:
            print(f"  {src}: {cnt}")
        cursor.close()
        db.close()
    else:
        print("=== Done (dry-run, no DB connection). ===")

if __name__ == "__main__":
    run()
