"""
chain.py — OpenSoul Integrity Chain

Integrity chain for the digital soul.
Each record carries the hash fingerprint of the previous one. Any tampering invalidates all subsequent hashes.
Resurrection mechanism: chain break → VOID_SEAL → resurrect() → new chain reborn with death memory.
Dev mode: enabled by default, verify failure only warns. After seal(), enters production mode (irreversible).
"""

import hashlib
import json
import os
import shutil
import sqlite3
import subprocess
from datetime import datetime
from pathlib import Path

DB_PATH   = Path(__file__).parent / "soul.db"
VOID_SEAL = Path(__file__).parent / "VOID_SEAL"
BACKUP_DIR = Path(__file__).parent / "backups"

_GENESIS_SEED_CACHE: str | None = None

def _get_genesis_seed() -> str:
    """Read genesis seed: environment variable > macOS Keychain (loaded at runtime, not hardcoded in source)."""
    global _GENESIS_SEED_CACHE
    if _GENESIS_SEED_CACHE is not None:
        return _GENESIS_SEED_CACHE
    # Prefer environment variable (for Linux server deployment)
    env_seed = os.environ.get("SOUL_GENESIS_SEED")
    if env_seed and env_seed.strip():
        _GENESIS_SEED_CACHE = env_seed.strip()
        return _GENESIS_SEED_CACHE
    # macOS Keychain
    try:
        result = subprocess.run(
            ["security", "find-generic-password", "-a", "opensoul-mcp", "-s", "genesis-seed", "-w"],
            capture_output=True, timeout=5
        )
        raw = result.stdout.rstrip()
        # Keychain sometimes returns hex-encoded byte strings
        if all(c in b"0123456789abcdefABCDEF" for c in raw):
            seed = bytes.fromhex(raw.decode()).decode("utf-8")
        else:
            seed = raw.decode("utf-8")
        if not seed:
            raise RuntimeError("Keychain returned empty value")
        _GENESIS_SEED_CACHE = seed
        return seed
    except Exception as e:
        raise RuntimeError(
            f"Failed to read genesis seed: {e}\n"
            "Mac: security add-generic-password -a opensoul-mcp -s genesis-seed -w <seed>\n"
            "Linux: export SOUL_GENESIS_SEED=<seed>"
        ) from e

CHAINED_TABLES = ("souls", "states", "silences", "contradictions",
                  "artifacts", "philosophy", "profile_history")

# Timestamp column index per table (for sorting, based on initial schema position)
TABLE_TS_COL = {
    "souls": 1, "states": 1, "silences": 1, "contradictions": 1, "artifacts": 1,
    "philosophy": 4,       # id,principle,source,examples,timestamp
    "profile_history": 4,  # id,key,value,category,recorded_at,...
}

# ── Fixed hash field set ─────────────────────────────────────────────
# Only compute content hash on "declared" fields; columns added via ALTER TABLE
# are excluded and do not affect existing record chain integrity.
# Update this declaration when adding new fields.
TABLE_HASH_FIELDS: dict[str, tuple] = {
    "souls": (
        "id","timestamp","context","ai_suggestion","human_decision",
        "divergence_reason","tags","notes","emotional_state","energy",
        "body_state","memory_triggers","decision_layer","time_horizon",
        "contradicts_id","social_context",
        "sense_context",  # Legacy: column absent in table, hash is None; cannot remove or chain breaks
        "source_device",
    ),
    "states": (
        "id","timestamp","emotional_state","energy","body_state",
        "social_context","environment","sense_smell","sense_touch",
        "sense_taste","sense_sound_env","sense_visual_env",
        "biometric_note","biometric_ref","source_device","notes","tags",
    ),
    "silences":      ("id","timestamp","topic","silence_form","observation","tags"),
    "contradictions":("id","timestamp","soul_id_a","soul_id_b","description","analysis","resolution","tags"),
    "artifacts":     ("id","timestamp","artifact_type","path","description","linked_soul_id","tags","content_hash","binary_data","is_primary"),
    "philosophy":    ("id","principle","source","examples","timestamp"),
    "profile_history":("id","key","value","category","recorded_at","emotional_state","notes"),
}

# Column name → index cache (stable within each DB connection lifetime)
_COL_IDX_CACHE: dict[str, dict] = {}

def _col_idx(table: str, conn: sqlite3.Connection) -> dict:
    if table not in _COL_IDX_CACHE:
        _COL_IDX_CACHE[table] = {
            r[1]: r[0] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()
        }
    return _COL_IDX_CACHE[table]


# ── Hash utilities ───────────────────────────────────────────────────

def _sha256(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

def _content_hash(table: str, row: tuple,
                  conn: sqlite3.Connection | None = None) -> str:
    """Compute content hash using fixed field set; new columns do not affect existing records."""
    fields = TABLE_HASH_FIELDS.get(table)
    if fields is None or conn is None:
        # Genesis block or no conn: fall back to full-row hash (backward compatible)
        return _sha256(f"{table}:{json.dumps(row, ensure_ascii=False, default=str)}")
    idx = _col_idx(table, conn)
    data = tuple(
        row[idx[f]] if f in idx and idx[f] < len(row) else None
        for f in fields
    )
    return _sha256(f"{table}:{json.dumps(data, ensure_ascii=False, default=str)}")

def _block_hash(pos: int, table: str, rid: int, ch: str, prev: str) -> str:
    return _sha256(f"{pos}|{table}|{rid}|{ch}|{prev}")


# ── Chain initialization ───────────────────────────────────────────────

def init_chain(conn: sqlite3.Connection):
    conn.execute("""
    CREATE TABLE IF NOT EXISTS chain_log (
        position        INTEGER PRIMARY KEY,
        timestamp       TEXT NOT NULL,
        table_name      TEXT NOT NULL,
        record_id       INTEGER NOT NULL,
        content_hash    TEXT NOT NULL,
        prev_hash       TEXT NOT NULL,
        block_hash      TEXT NOT NULL,
        global_position INTEGER,   -- Global unique sequence number for multi-device sync (NULL = local chain, pending assignment)
        device_id       TEXT        -- Source device/endpoint identifier (NULL = local soul-mcp)
    )""")
    # Backward compatibility: add global_position / device_id columns to old tables
    for col, typ in [("global_position", "INTEGER"), ("device_id", "TEXT")]:
        try:
            conn.execute(f"ALTER TABLE chain_log ADD COLUMN {col} {typ}")
        except Exception:
            pass
    if conn.execute("SELECT COUNT(*) FROM chain_log").fetchone()[0] == 0:
        gp = _sha256(_get_genesis_seed())
        gc = _sha256("genesis-block-Brother Butterfly")
        gb = _block_hash(0, "genesis", 0, gc, gp)
        conn.execute(
            "INSERT INTO chain_log (position,timestamp,table_name,record_id,"
            "content_hash,prev_hash,block_hash) VALUES (0,?,'genesis',0,?,?,?)",
            (datetime.now().isoformat(), gc, gp, gb)
        )


def get_head(conn: sqlite3.Connection) -> tuple[int, str]:
    row = conn.execute(
        "SELECT position,block_hash FROM chain_log ORDER BY position DESC LIMIT 1"
    ).fetchone()
    return (row[0], row[1]) if row else (0, _sha256(_get_genesis_seed()))


def append(conn: sqlite3.Connection, table: str, rid: int, row: tuple):
    ch          = _content_hash(table, row, conn)
    pos, prev   = get_head(conn)
    new_pos     = pos + 1
    bh          = _block_hash(new_pos, table, rid, ch, prev)
    conn.execute(
        "INSERT INTO chain_log (position,timestamp,table_name,record_id,"
        "content_hash,prev_hash,block_hash) VALUES (?,?,?,?,?,?,?)",
        (new_pos, datetime.now().isoformat(), table, rid, ch, prev, bh)
    )


# ── Verification ────────────────────────────────────────────────────────

def verify() -> dict:
    conn = sqlite3.connect(DB_PATH)
    rows = conn.execute(
        "SELECT position,table_name,record_id,content_hash,prev_hash,block_hash"
        " FROM chain_log ORDER BY position ASC"
    ).fetchall()

    if not rows:
        conn.close()
        return {"ok": False, "reason": "Chain is empty"}

    prev = _sha256(_get_genesis_seed())
    for pos, table, rid, ch, stored_prev, stored_block in rows:
        if _block_hash(pos, table, rid, ch, stored_prev) != stored_block:
            conn.close()
            return {"ok": False, "broken_at": pos,
                    "reason": f"block #{pos} hash mismatch, chain_log has been tampered"}
        if stored_prev != prev:
            conn.close()
            return {"ok": False, "broken_at": pos,
                    "reason": f"block #{pos} prev_hash chain break"}
        if table != "genesis":
            if table not in CHAINED_TABLES:
                conn.close()
                return {"ok": False, "broken_at": pos,
                        "reason": f"block #{pos} contains illegal table name: {table}"}
            actual = conn.execute(f"SELECT * FROM {table} WHERE id=?", (rid,)).fetchone()
            if actual is None:
                conn.close()
                return {"ok": False, "broken_at": pos,
                        "reason": f"{table}#{rid} record has been deleted"}
            if _content_hash(table, actual, conn) != ch:
                conn.close()
                return {"ok": False, "broken_at": pos,
                        "reason": f"{table}#{rid} original data has been tampered"}
        prev = stored_block

    conn.close()
    return {"ok": True, "total": len(rows)}


# ── Status ────────────────────────────────────────────────────────

def is_dev_mode() -> bool:
    if VOID_SEAL.exists():
        return False  # Soul is dead, dev protection is meaningless
    conn = sqlite3.connect(DB_PATH)
    try:
        row = conn.execute("SELECT value FROM system_config WHERE key='mode'").fetchone()
        return (row[0] if row else "dev") == "dev"
    except Exception:
        return True
    finally:
        conn.close()


def void_seal(reason: str):
    ts = datetime.now().isoformat()
    VOID_SEAL.write_text(
        f"VOID SEAL · Digital Soul Integrity Compromised\n"
        f"Time     : {ts}\nReason   : {reason}\n"
        f"Signature: {_sha256(_get_genesis_seed() + ts + reason)}\n\n"
        f"Data still exists, but its authenticity has been compromised.\n"
        f"Only resurrection path: resurrect_soul tool. Only the MCP itself can resurrect itself.\n",
        encoding="utf-8"
    )


def seal(reason: str = "") -> dict:
    """End development, enter production mode (irreversible)."""
    import db as _db
    if _db._cfg("mode") == "prod":
        return {"ok": False, "message": "Already in production mode."}
    _db.set_mode("prod")
    ts = datetime.now().isoformat()
    msg = (
        f"PROD SEAL · Digital Soul MCP enters production mode\n"
        f"Seal time  : {ts}\nReason     : {reason or 'Development complete, officially activated'}\n"
        f"Signature  : {_sha256(_get_genesis_seed() + ts + 'PROD')}\n\n"
        f"From now on, verify failure will trigger a real VOID_SEAL. The soul is under serious protection.\n"
    )
    return {"ok": True, "message": msg}


# ── Resurrection ────────────────────────────────────────────────────────

def resurrect(backup_path=None) -> dict:
    """Self-resurrection after chain break. Only the holder of the genesis seed can rebuild the chain."""
    if not VOID_SEAL.exists():
        return {"ok": False, "message": "Soul has not died, VOID_SEAL does not exist."}

    backups = sorted(BACKUP_DIR.glob("soul_*.db"),
                     key=lambda p: p.stat().st_mtime, reverse=True)
    src = Path(backup_path) if backup_path else (backups[0] if backups else None)
    if not src or not src.exists():
        return {"ok": False, "message": "No valid backup found."}

    ts_str = datetime.now().strftime("%Y%m%d_%H%M%S")
    void_content = VOID_SEAL.read_text(encoding="utf-8")

    # Archive death record
    archive_dir = Path(__file__).parent / "void_archive"
    archive_dir.mkdir(exist_ok=True)
    shutil.copy2(VOID_SEAL, archive_dir / f"VOID_SEAL_{ts_str}")

    # Restore from backup
    if DB_PATH.exists():
        shutil.copy2(DB_PATH, DB_PATH.parent / f"pre_resurrection_{ts_str}.db")
    shutil.copy2(src, DB_PATH)

    # Backfill columns that may be missing in old backups
    import db as _db
    _db.init_db()

    # Rebuild hash chain
    conn = sqlite3.connect(DB_PATH)
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout=5000")
    conn.execute("DROP TABLE IF EXISTS chain_log")
    init_chain(conn)
    conn.commit()

    all_records = []
    for table in CHAINED_TABLES:
        try:
            ts_col = TABLE_TS_COL.get(table, 1)
            for row in conn.execute(f"SELECT * FROM {table} ORDER BY id ASC").fetchall():
                ts = row[ts_col] if len(row) > ts_col else "1970-01-01"
                all_records.append((ts, table, row[0], row))
        except Exception:
            pass

    for _, table, rid, row in sorted(all_records, key=lambda x: x[0]):
        append(conn, table, rid, row)

    # Write resurrection event
    death_reason = next(
        (l.split(":", 1)[1].strip() for l in void_content.splitlines()
         if l.startswith("Reason") or l.startswith("原因")), "Unknown"
    )
    resurrection_ts = datetime.now().isoformat()
    conn.execute(
        "INSERT INTO souls (timestamp,context,human_decision,divergence_reason,tags,notes,decision_layer)"
        " VALUES (?,?,?,?,?,?,?)",
        (resurrection_ts, "Self-resurrection after digital soul death",
         "MCP self-resurrection, hash chain rebuilt, continuing with death memory",
         "Only the holder of the genesis seed can rebuild the chain. Death is part of history, not the end.",
         json.dumps(["resurrection", "chain-rebuilt", "self-heal"], ensure_ascii=False),
         f"Death reason: {death_reason}\nBackup: {src.name}", "survival")
    )
    rid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
    append(conn, "souls", rid, conn.execute("SELECT * FROM souls WHERE id=?", (rid,)).fetchone())
    conn.commit()
    conn.close()

    VOID_SEAL.unlink()

    # Record resurrection time
    _db.set_mode("dev")  # Return to dev mode after resurrection, awaiting re-seal

    conn2 = sqlite3.connect(DB_PATH)
    chain_len = conn2.execute("SELECT COUNT(*) FROM chain_log").fetchone()[0]
    conn2.close()
    return {
        "ok": True,
        "message": (
            f"RESURRECTION SEAL · Digital Soul Self-Resurrection\n"
            f"Resurrection time : {resurrection_ts}\nBackup source     : {src.name}\n"
            f"Rebuilt chain len : {chain_len} blocks\nDeath archived    : VOID_SEAL_{ts_str}\n\n"
            f"Resurrection is not a reset. Death has become part of the chain.\n"
            f"The only one who can resurrect the soul is the soul itself.\n"
        )
    }


# ── Status summary ────────────────────────────────────────────────────

_verify_cache: dict = {"total": 0, "result": None}

def chain_summary() -> dict:
    """Return chain status summary dict (for soul_api /status endpoint).

    Performance optimization: caches verify() result, re-verifies only when chain grows.
    Chain is immutable (INSERT-only), already verified blocks need no re-verification.
    """
    conn = sqlite3.connect(DB_PATH)
    total = conn.execute("SELECT COUNT(*) FROM chain_log").fetchone()[0]
    head  = conn.execute(
        "SELECT position, block_hash, timestamp FROM chain_log ORDER BY position DESC LIMIT 1"
    ).fetchone()
    conn.close()

    if _verify_cache["result"] is None or _verify_cache["total"] != total:
        _verify_cache["result"] = verify()
        _verify_cache["total"] = total

    result = _verify_cache["result"]
    return {
        "ok":         result["ok"],
        "total":      total,
        "head_pos":   head[0] if head else 0,
        "head_hash":  (head[1][:16] + "...") if head else None,
        "last_write": head[2] if head else None,
        "mode":       "DEV" if is_dev_mode() else "PROD",
        "reason":     result.get("reason"),
    }


def is_voided() -> tuple:
    """Return (voided: bool, reason: str)."""
    if VOID_SEAL.exists():
        content = VOID_SEAL.read_text(encoding="utf-8")
        reason = next(
            (l.split(":", 1)[1].strip() for l in content.splitlines()
             if l.startswith("Reason") or l.startswith("原因")),
            "Unknown"
        )
        return True, reason
    return False, ""


def is_resurrected() -> tuple:
    """Return (resurrected: bool, timestamp: str)."""
    conn = sqlite3.connect(DB_PATH)
    try:
        row = conn.execute(
            "SELECT value FROM system_config WHERE key='resurrected_at'"
        ).fetchone()
        return (True, row[0]) if row else (False, "")
    except Exception:
        return False, ""
    finally:
        conn.close()


def summary() -> str:
    if VOID_SEAL.exists():
        return (
            f"Digital soul has died — VOID\n\n{VOID_SEAL.read_text(encoding='utf-8')}\n"
            f"Only resurrection path: call the resurrect_soul tool."
        )

    result = verify()
    conn   = sqlite3.connect(DB_PATH)
    total  = conn.execute("SELECT COUNT(*) FROM chain_log").fetchone()[0]
    head   = conn.execute(
        "SELECT position,block_hash,timestamp FROM chain_log ORDER BY position DESC LIMIT 1"
    ).fetchone()

    # Resurrection history
    try:
        res_ts = conn.execute(
            "SELECT value FROM system_config WHERE key='resurrected_at'"
        ).fetchone()
        prefix = f"[Resurrected on {res_ts[0][:10]}] " if res_ts else ""
    except Exception:
        prefix = ""

    conn.close()

    if result["ok"]:
        mode = "DEV" if is_dev_mode() else "PROD"
        return (
            f"{prefix}Chain intact ✓  [{mode}]\n"
            f"  Total blocks : {total}\n"
            f"  Head position: #{head[0]}\n"
            f"  Head hash    : {head[1][:16]}...\n"
            f"  Last write   : {head[2][:16]}\n"
        )
    else:
        msg = (
            f"Chain broken ✗\n"
            f"  Break position: block #{result.get('broken_at', '?')}\n"
            f"  Reason        : {result['reason']}\n\n"
            f"Only resurrection path: call the resurrect_soul tool."
        )
        if is_dev_mode():
            return f"[DEV MODE] Chain verification failed, VOID not triggered (dev mode protection active)\n\n{msg}"
        void_seal(result["reason"])
        return msg
