# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
L2 Chain Anchor

Abstract interface for anchoring Merkle roots to Layer 2 chains.
Provides a local SQLite ledger (always available) and stubbed
implementations for Arbitrum and Solana (require web3.py / solders).

The LocalLedger serves as the production-ready default. L2 anchoring
can be enabled later without data loss — all local anchors are preserved
and can be replayed to L2 when configured.

Dependencies: None for LocalLedger. web3.py or solders for L2 stubs.
"""

import json
import logging
import sqlite3
from abc import ABC, abstractmethod
from datetime import datetime
from pathlib import Path

logger = logging.getLogger(__name__)


class L2Anchor(ABC):
    """Abstract base for L2 chain anchoring."""

    @abstractmethod
    def anchor_root(self, merkle_root: str, metadata: dict) -> str:
        """
        Anchor a Merkle root with metadata to the chain.

        Args:
            merkle_root: Hex-encoded Merkle root hash
            metadata: Additional metadata (block range, depth, etc.)

        Returns:
            Transaction hash or local record ID
        """

    @abstractmethod
    def verify_root(self, tx_hash: str) -> dict:
        """
        Verify an anchored Merkle root.

        Args:
            tx_hash: Transaction hash or record ID from anchor_root()

        Returns:
            Dict with verification result and metadata
        """

    @abstractmethod
    def get_balance(self, address: str) -> dict:
        """
        Get token balance for an address.

        Args:
            address: Wallet or genesis hash address

        Returns:
            Dict with balance information
        """


class LocalLedger(L2Anchor):
    """
    SQLite-backed local ledger — always available, no dependencies.

    Stores anchored Merkle roots locally. Can be replayed to L2 later.
    """

    def __init__(self, db_path: Path = None):
        if db_path is None:
            try:
                from familiar.core.paths import CHAIN_DIR

                db_path = CHAIN_DIR / "anchors.db"
            except ImportError:
                db_path = Path.home() / ".familiar" / "data" / "chain" / "anchors.db"

        db_path.parent.mkdir(parents=True, exist_ok=True)
        self._db_path = db_path
        self._init_db()

    def _init_db(self):
        with sqlite3.connect(str(self._db_path)) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS anchors (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    tx_hash TEXT UNIQUE NOT NULL,
                    merkle_root TEXT NOT NULL,
                    metadata TEXT NOT NULL,
                    anchored_at TEXT NOT NULL,
                    synced_to_l2 INTEGER DEFAULT 0,
                    l2_tx_hash TEXT DEFAULT ''
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS balances (
                    address TEXT PRIMARY KEY,
                    balance REAL DEFAULT 0.0,
                    last_updated TEXT NOT NULL
                )
            """)

    def anchor_root(self, merkle_root: str, metadata: dict) -> str:
        """Anchor a Merkle root to the local ledger."""
        import hashlib

        now = datetime.now().isoformat()
        tx_hash = hashlib.sha256(f"{merkle_root}:{now}".encode()).hexdigest()[:32]

        with sqlite3.connect(str(self._db_path)) as conn:
            conn.execute(
                """INSERT INTO anchors (tx_hash, merkle_root, metadata, anchored_at)
                   VALUES (?, ?, ?, ?)""",
                (tx_hash, merkle_root, json.dumps(metadata, default=str), now),
            )

        logger.info("Anchored Merkle root %s as %s", merkle_root[:16], tx_hash[:16])
        return tx_hash

    def verify_root(self, tx_hash: str) -> dict:
        """Verify an anchored Merkle root."""
        with sqlite3.connect(str(self._db_path)) as conn:
            row = conn.execute(
                "SELECT merkle_root, metadata, anchored_at, synced_to_l2, l2_tx_hash "
                "FROM anchors WHERE tx_hash = ?",
                (tx_hash,),
            ).fetchone()

        if row is None:
            return {"verified": False, "error": "Anchor not found"}

        return {
            "verified": True,
            "merkle_root": row[0],
            "metadata": json.loads(row[1]) if row[1] else {},
            "anchored_at": row[2],
            "synced_to_l2": bool(row[3]),
            "l2_tx_hash": row[4] or "",
        }

    def get_balance(self, address: str) -> dict:
        """Get Dross balance from local ledger."""
        with sqlite3.connect(str(self._db_path)) as conn:
            row = conn.execute(
                "SELECT balance, last_updated FROM balances WHERE address = ?",
                (address,),
            ).fetchone()

        if row is None:
            return {"address": address, "balance": 0.0, "last_updated": ""}

        return {
            "address": address,
            "balance": row[0],
            "last_updated": row[1],
        }

    def update_balance(self, address: str, amount: float):
        """Update balance for an address."""
        now = datetime.now().isoformat()
        with sqlite3.connect(str(self._db_path)) as conn:
            conn.execute(
                """INSERT INTO balances (address, balance, last_updated)
                   VALUES (?, ?, ?)
                   ON CONFLICT(address) DO UPDATE SET
                   balance = balance + ?, last_updated = ?""",
                (address, amount, now, amount, now),
            )

    def get_unsynced_anchors(self, limit: int = 100) -> list:
        """Get anchors not yet synced to L2."""
        with sqlite3.connect(str(self._db_path)) as conn:
            rows = conn.execute(
                "SELECT tx_hash, merkle_root, metadata, anchored_at "
                "FROM anchors WHERE synced_to_l2 = 0 ORDER BY id LIMIT ?",
                (limit,),
            ).fetchall()

        return [
            {
                "tx_hash": r[0],
                "merkle_root": r[1],
                "metadata": json.loads(r[2]) if r[2] else {},
                "anchored_at": r[3],
            }
            for r in rows
        ]

    def mark_synced(self, tx_hash: str, l2_tx_hash: str):
        """Mark an anchor as synced to L2."""
        with sqlite3.connect(str(self._db_path)) as conn:
            conn.execute(
                "UPDATE anchors SET synced_to_l2 = 1, l2_tx_hash = ? WHERE tx_hash = ?",
                (l2_tx_hash, tx_hash),
            )


class ArbitrumAnchor(L2Anchor):
    """
    Arbitrum L2 anchoring — stubbed until web3.py is available.

    When enabled, anchors Merkle roots to Arbitrum via smart contract.
    """

    def __init__(self, rpc_url: str = "", contract_address: str = "", private_key: str = ""):
        self._rpc_url = rpc_url
        self._contract = contract_address
        self._key = private_key

    def anchor_root(self, merkle_root: str, metadata: dict) -> str:
        raise NotImplementedError(
            "Arbitrum anchoring requires web3.py. Install with: pip install 'familiar-agent[chain]'"
        )

    def verify_root(self, tx_hash: str) -> dict:
        raise NotImplementedError("Arbitrum verification requires web3.py")

    def get_balance(self, address: str) -> dict:
        raise NotImplementedError("Arbitrum balance check requires web3.py")


class SolanaAnchor(L2Anchor):
    """
    Solana anchoring — stubbed until solders is available.

    When enabled, anchors Merkle roots to Solana via program instruction.
    """

    def __init__(self, rpc_url: str = "", program_id: str = "", keypair_path: str = ""):
        self._rpc_url = rpc_url
        self._program_id = program_id
        self._keypair_path = keypair_path

    def anchor_root(self, merkle_root: str, metadata: dict) -> str:
        raise NotImplementedError(
            "Solana anchoring requires solders. Install with: pip install solders"
        )

    def verify_root(self, tx_hash: str) -> dict:
        raise NotImplementedError("Solana verification requires solders")

    def get_balance(self, address: str) -> dict:
        raise NotImplementedError("Solana balance check requires solders")
