"""
Local storage for Verisoo.

Two-layer storage architecture:
1. Binary append-only log (binlog.py) - stores raw records, federation-friendly
2. LMDB index (lmdb_store.py) - fast lookups by attestor, image hash, etc.

Directory structure:
    ~/.verisoo/
    ├── config.json          # Node configuration
    ├── identity/
    │   ├── private.pem      # Node's private key (encrypted)
    │   └── public.pem       # Node's public key
    ├── log.bin              # Append-only binary record log
    ├── index/               # LMDB index database
    │   ├── data.mdb
    │   └── lock.mdb
    └── peers.json           # Federation peer list
"""

from __future__ import annotations

import hashlib
import json
from datetime import datetime
from pathlib import Path
from typing import Iterator

from .binlog import BinaryLog
from .exceptions import VerisooError
from .lmdb_store import LMDBIndex, IndexStats
from .merkle import MerkleLog
from .models import AttestationRecord, Identity, ImageHashes


class StorageError(VerisooError):
    """Error in storage operations."""

    pass


def _serialize_record(record: AttestationRecord) -> bytes:
    """Serialize a record to bytes for storage."""
    data = {
        "sha256": record.image_hashes.sha256,
        "phash": record.image_hashes.phash,
        "dhash": record.image_hashes.dhash,
        "signature": record.signature.hex(),
        "attestor": record.attestor_fingerprint,
        "timestamp": record.timestamp.isoformat(),
        "metadata": record.metadata,
    }
    return json.dumps(data, sort_keys=True, separators=(",", ":")).encode("utf-8")


def _deserialize_record(data: bytes) -> AttestationRecord:
    """Deserialize a record from bytes."""
    obj = json.loads(data.decode("utf-8"))
    return AttestationRecord(
        image_hashes=ImageHashes(
            sha256=obj["sha256"],
            phash=obj["phash"],
            dhash=obj["dhash"],
        ),
        signature=bytes.fromhex(obj["signature"]),
        attestor_fingerprint=obj["attestor"],
        timestamp=datetime.fromisoformat(obj["timestamp"]),
        metadata=obj.get("metadata", {}),
    )


def _compute_record_id(record: AttestationRecord) -> str:
    """Compute unique ID for a record."""
    # Hash of (sha256 || attestor || timestamp)
    content = f"{record.image_hashes.sha256}|{record.attestor_fingerprint}|{record.timestamp.isoformat()}"
    return hashlib.sha256(content.encode()).hexdigest()[:32]


class LocalStorage:
    """
    High-level storage interface combining binary log and LMDB index.

    Thread-safe for reads, single-writer for appends.
    """

    DEFAULT_PATH = Path.home() / ".verisoo"

    def __init__(self, base_path: Path | None = None) -> None:
        self.base_path = base_path or self.DEFAULT_PATH
        self._ensure_directories()

        # Initialize storage layers
        self._binlog = BinaryLog(self.base_path / "log.bin")
        self._index = LMDBIndex(self.base_path / "index")

        # Verify consistency between log and index
        self._verify_consistency()

    def _ensure_directories(self) -> None:
        """Create directory structure if needed."""
        dirs = [
            self.base_path,
            self.base_path / "identity",
        ]
        for d in dirs:
            d.mkdir(parents=True, exist_ok=True)

        # Restrict permissions on sensitive directories
        try:
            self.base_path.chmod(0o700)
            (self.base_path / "identity").chmod(0o700)
        except OSError:
            pass

    def _verify_consistency(self) -> None:
        """Verify log and index are in sync."""
        log_size = self._binlog.size
        index_size = self._index.get_record_count()

        if log_size > index_size:
            # Index is behind - rebuild missing entries
            self._rebuild_index_from(index_size)
        elif index_size > log_size:
            # Index is ahead (shouldn't happen) - reset index count
            self._index.set_record_count(log_size)

    def _rebuild_index_from(self, start_index: int) -> None:
        """Rebuild index entries from log starting at index."""
        for entry in self._binlog.iterate():
            if entry.index >= start_index:
                record = _deserialize_record(entry.data)
                record_id = _compute_record_id(record)
                self._index.index_record(entry.index, record, record_id)

        self._index.set_record_count(self._binlog.size)

    def close(self) -> None:
        """Close storage (flushes LMDB)."""
        self._index.close()

    def __enter__(self) -> LocalStorage:
        return self

    def __exit__(self, *args) -> None:
        self.close()

    # --- Record operations ---

    def append_record(self, record: AttestationRecord) -> int:
        """
        Append a record to the log and index it.

        Returns the index of the new record.
        """
        # Serialize and append to binary log
        data = _serialize_record(record)
        index = self._binlog.append(data)

        # Index the record
        record_id = _compute_record_id(record)
        self._index.index_record(index, record, record_id)
        self._index.set_record_count(self._binlog.size)

        return index

    def get_record(self, index: int) -> AttestationRecord:
        """Get record by index."""
        data = self._binlog.read(index)
        return _deserialize_record(data)

    def get_records_range(self, start: int, end: int) -> list[AttestationRecord]:
        """Get records in range [start, end)."""
        data_list = self._binlog.read_range(start, end)
        return [_deserialize_record(d) for d in data_list]

    def iterate_records(self) -> Iterator[AttestationRecord]:
        """Stream all records."""
        for entry in self._binlog.iterate():
            yield _deserialize_record(entry.data)

    @property
    def record_count(self) -> int:
        """Number of records in the log."""
        return self._binlog.size

    # --- Query operations (via index) ---

    def get_records_by_attestor(self, fingerprint: str) -> list[AttestationRecord]:
        """Get all records by an attestor."""
        indices = self._index.get_indices_by_attestor(fingerprint)
        return [self.get_record(i) for i in indices]

    def get_records_by_image_sha256(self, sha256: str) -> list[AttestationRecord]:
        """Get all records for an image by exact SHA-256."""
        indices = self._index.get_indices_by_sha256(sha256)
        return [self.get_record(i) for i in indices]

    def get_records_by_image_phash(self, phash: str) -> list[AttestationRecord]:
        """Get all records for an image by perceptual hash."""
        indices = self._index.get_indices_by_phash(phash)
        return [self.get_record(i) for i in indices]

    def find_similar_images(
        self, phash: str, max_distance: int = 10
    ) -> list[tuple[AttestationRecord, int]]:
        """
        Find records for perceptually similar images.

        Returns list of (record, hamming_distance) tuples.
        """
        similar = self._index.find_similar_phash(phash, max_distance)
        results = []
        for found_phash, distance in similar:
            indices = self._index.get_indices_by_phash(found_phash)
            for idx in indices:
                results.append((self.get_record(idx), distance))
        return results

    # --- Merkle tree ---

    def load_merkle_log(self) -> MerkleLog:
        """
        Load records into a MerkleLog for verification.

        Note: This loads all records into memory. For large logs,
        use iterate_records() and build incrementally.
        """
        log = MerkleLog()
        for record in self.iterate_records():
            log.append(record)
        return log

    def get_merkle_root(self) -> str | None:
        """Get stored merkle root (may be stale - use load_merkle_log for current)."""
        return self._index.get_merkle_root()

    def set_merkle_root(self, root: str) -> None:
        """Store merkle root after computing."""
        self._index.set_merkle_root(root)

    # --- Federation sync ---

    def get_sync_bytes(self, from_offset: int) -> bytes:
        """
        Get raw bytes for federation sync.

        Returns bytes from the binary log starting at offset.
        Peer can append these directly to their log.
        """
        return self._binlog.get_bytes_from_offset(from_offset)

    def append_sync_bytes(self, data: bytes) -> int:
        """
        Append pre-formatted records from federation sync.

        Returns number of records added.
        """
        start_index = self._binlog.size
        records_added = self._binlog.append_raw_bytes(data)

        # Index the new records
        self._rebuild_index_from(start_index)

        return records_added

    @property
    def log_file_size(self) -> int:
        """Size of the binary log file in bytes."""
        return self._binlog.file_size

    # --- Identity management ---

    def save_identity(self, identity: Identity) -> None:
        """Save a known identity to the index."""
        self._index.save_identity(identity)

    def load_identity(self, fingerprint: str) -> Identity | None:
        """Load identity by fingerprint."""
        return self._index.get_identity(fingerprint)

    def list_identities(self) -> list[str]:
        """List all known identity fingerprints."""
        return self._index.list_identities()

    # --- Node identity (keypair files) ---

    @property
    def identity_path(self) -> Path:
        return self.base_path / "identity"

    @property
    def private_key_path(self) -> Path:
        return self.identity_path / "private.pem"

    @property
    def public_key_path(self) -> Path:
        return self.identity_path / "public.pem"

    def has_node_identity(self) -> bool:
        """Check if this node has an identity configured."""
        return self.private_key_path.exists()

    # --- Peers ---

    def save_peers(self, peers: list[dict]) -> None:
        """Save peer list."""
        path = self.base_path / "peers.json"
        path.write_text(json.dumps(peers, indent=2))

    def load_peers(self) -> list[dict]:
        """Load peer list."""
        path = self.base_path / "peers.json"
        if not path.exists():
            return []
        return json.loads(path.read_text())

    # --- Config ---

    def save_config(self, config: dict) -> None:
        """Save node configuration."""
        path = self.base_path / "config.json"
        path.write_text(json.dumps(config, indent=2))

    def load_config(self) -> dict:
        """Load node configuration."""
        path = self.base_path / "config.json"
        if not path.exists():
            return {}
        return json.loads(path.read_text())

    # --- Stats ---

    def get_stats(self) -> IndexStats:
        """Get storage statistics."""
        stats = self._index.get_stats()
        stats.log_file_size = self._binlog.file_size
        return stats
