"""
Genesis Atomic I/O
==================
Atomic file operations to prevent data corruption.

Uses the "write to temp, then rename" pattern for atomic writes.
Prevents partial writes and ensures data integrity.

Usage:
    from atomic_io import atomic_write, atomic_json_write, safe_read

    # Atomic write
    atomic_write("/path/to/file.txt", "content")

    # Atomic JSON write
    atomic_json_write("/path/to/data.json", {"key": "value"})

    # Safe read with fallback
    data = safe_read("/path/to/file.txt", default="")
"""

import os
import json
import tempfile
import shutil
from pathlib import Path
from typing import Any, Optional, Union, Callable
from datetime import datetime
import threading
import hashlib

# Global lock for file operations
_file_locks: dict = {}
_lock_manager = threading.Lock()


def _get_file_lock(path: str) -> threading.Lock:
    """Get or create a lock for a specific file path."""
    with _lock_manager:
        if path not in _file_locks:
            _file_locks[path] = threading.Lock()
        return _file_locks[path]


def atomic_write(
    file_path: Union[str, Path],
    content: Union[str, bytes],
    mode: str = 'w',
    backup: bool = True
) -> bool:
    """
    Atomically write content to a file.

    Uses temp file + rename pattern for atomicity.
    Creates backup of existing file.

    Args:
        file_path: Target file path
        content: Content to write (str or bytes)
        mode: Write mode ('w' for text, 'wb' for binary)
        backup: Create .bak backup of existing file

    Returns:
        True if successful, False otherwise
    """
    file_path = Path(file_path)
    lock = _get_file_lock(str(file_path))

    with lock:
        try:
            # Ensure parent directory exists
            file_path.parent.mkdir(parents=True, exist_ok=True)

            # Create temp file in same directory (for same-filesystem rename)
            temp_fd, temp_path = tempfile.mkstemp(
                dir=file_path.parent,
                prefix=f".{file_path.stem}_",
                suffix=".tmp"
            )

            try:
                # Write to temp file
                with os.fdopen(temp_fd, mode) as f:
                    f.write(content)
                    f.flush()
                    os.fsync(f.fileno())  # Force write to disk

                # Create backup if file exists
                if backup and file_path.exists():
                    backup_path = file_path.with_suffix(f"{file_path.suffix}.bak")
                    shutil.copy2(file_path, backup_path)

                # Atomic rename (POSIX guarantees this is atomic)
                if os.name == 'nt':  # Windows
                    # Windows doesn't support atomic rename over existing file
                    if file_path.exists():
                        file_path.unlink()
                    os.rename(temp_path, file_path)
                else:  # POSIX
                    os.rename(temp_path, file_path)

                return True

            except Exception as e:
                # Clean up temp file on error
                if os.path.exists(temp_path):
                    os.unlink(temp_path)
                raise

        except Exception as e:
            print(f"[!] Atomic write failed for {file_path}: {e}")
            return False


def atomic_json_write(
    file_path: Union[str, Path],
    data: Any,
    indent: int = 2,
    backup: bool = True
) -> bool:
    """
    Atomically write JSON data to a file.

    Args:
        file_path: Target file path
        data: JSON-serializable data
        indent: JSON indentation (default 2)
        backup: Create backup of existing file

    Returns:
        True if successful, False otherwise
    """
    try:
        content = json.dumps(data, indent=indent, default=str)
        return atomic_write(file_path, content, mode='w', backup=backup)
    except Exception as e:
        print(f"[!] Atomic JSON write failed: {e}")
        return False


def safe_read(
    file_path: Union[str, Path],
    default: Any = None,
    mode: str = 'r'
) -> Any:
    """
    Safely read a file with fallback to default.

    Args:
        file_path: File to read
        default: Default value if file doesn't exist or read fails
        mode: Read mode

    Returns:
        File content or default value
    """
    file_path = Path(file_path)

    try:
        if not file_path.exists():
            return default

        with open(file_path, mode) as f:
            return f.read()

    except Exception as e:
        print(f"[!] Safe read failed for {file_path}: {e}")
        return default


def safe_json_read(
    file_path: Union[str, Path],
    default: Any = None
) -> Any:
    """
    Safely read JSON from a file with fallback.

    Args:
        file_path: JSON file to read
        default: Default value if read fails

    Returns:
        Parsed JSON or default value
    """
    content = safe_read(file_path, default=None)
    if content is None:
        return default

    try:
        return json.loads(content)
    except json.JSONDecodeError as e:
        print(f"[!] JSON decode failed for {file_path}: {e}")
        return default


def atomic_update(
    file_path: Union[str, Path],
    update_fn: Callable[[Any], Any],
    default: Any = None,
    is_json: bool = True
) -> bool:
    """
    Atomically read, update, and write a file.

    Args:
        file_path: File to update
        update_fn: Function that takes current content and returns new content
        default: Default value if file doesn't exist
        is_json: Whether file is JSON format

    Returns:
        True if successful
    """
    file_path = Path(file_path)
    lock = _get_file_lock(str(file_path))

    with lock:
        try:
            # Read current content
            if is_json:
                current = safe_json_read(file_path, default=default)
            else:
                current = safe_read(file_path, default=default)

            # Apply update
            updated = update_fn(current)

            # Write atomically
            if is_json:
                return atomic_json_write(file_path, updated)
            else:
                return atomic_write(file_path, updated)

        except Exception as e:
            print(f"[!] Atomic update failed for {file_path}: {e}")
            return False


def verify_integrity(
    file_path: Union[str, Path],
    expected_hash: Optional[str] = None
) -> dict:
    """
    Verify file integrity.

    Args:
        file_path: File to verify
        expected_hash: Optional expected SHA256 hash

    Returns:
        Dict with exists, readable, hash, valid fields
    """
    file_path = Path(file_path)
    result = {
        "exists": False,
        "readable": False,
        "hash": None,
        "valid": False,
        "size": 0
    }

    if not file_path.exists():
        return result

    result["exists"] = True
    result["size"] = file_path.stat().st_size

    try:
        with open(file_path, 'rb') as f:
            content = f.read()
            result["readable"] = True
            result["hash"] = hashlib.sha256(content).hexdigest()

            if expected_hash:
                result["valid"] = result["hash"] == expected_hash
            else:
                result["valid"] = True

    except Exception as e:
        result["error"] = str(e)

    return result


def cleanup_temp_files(directory: Union[str, Path], max_age_hours: int = 24) -> int:
    """
    Clean up orphaned temp files from failed atomic writes.

    Args:
        directory: Directory to clean
        max_age_hours: Max age of temp files to keep

    Returns:
        Number of files cleaned
    """
    directory = Path(directory)
    if not directory.exists():
        return 0

    cleaned = 0
    cutoff = datetime.now().timestamp() - (max_age_hours * 3600)

    for temp_file in directory.glob(".*_*.tmp"):
        try:
            if temp_file.stat().st_mtime < cutoff:
                temp_file.unlink()
                cleaned += 1
        except Exception:
            pass

    if cleaned > 0:
        print(f"[OK] Cleaned {cleaned} orphaned temp files from {directory}")

    return cleaned


class AtomicFile:
    """
    Context manager for atomic file writes.

    Usage:
        with AtomicFile("/path/to/file.txt") as f:
            f.write("content")
        # File is atomically written on context exit
    """

    def __init__(
        self,
        file_path: Union[str, Path],
        mode: str = 'w',
        backup: bool = True
    ):
        self.file_path = Path(file_path)
        self.mode = mode
        self.backup = backup
        self.temp_fd = None
        self.temp_path = None
        self._file = None

    def __enter__(self):
        self.file_path.parent.mkdir(parents=True, exist_ok=True)

        self.temp_fd, self.temp_path = tempfile.mkstemp(
            dir=self.file_path.parent,
            prefix=f".{self.file_path.stem}_",
            suffix=".tmp"
        )

        self._file = os.fdopen(self.temp_fd, self.mode)
        return self._file

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            if self._file:
                self._file.flush()
                os.fsync(self._file.fileno())
                self._file.close()

            if exc_type is None:
                # No exception - commit the write
                if self.backup and self.file_path.exists():
                    backup_path = self.file_path.with_suffix(f"{self.file_path.suffix}.bak")
                    shutil.copy2(self.file_path, backup_path)

                if os.name == 'nt' and self.file_path.exists():
                    self.file_path.unlink()

                os.rename(self.temp_path, self.file_path)
            else:
                # Exception occurred - clean up temp file
                if os.path.exists(self.temp_path):
                    os.unlink(self.temp_path)

        except Exception as e:
            print(f"[!] AtomicFile cleanup error: {e}")
            if os.path.exists(self.temp_path):
                os.unlink(self.temp_path)

        return False  # Don't suppress exceptions


# CLI interface
if __name__ == "__main__":
    import sys

    if len(sys.argv) > 1:
        cmd = sys.argv[1]

        if cmd == "test":
            print("=== Atomic I/O Test ===\n")

            test_file = Path("/tmp/atomic_test.json")

            # Test atomic JSON write
            print("Testing atomic JSON write...")
            data = {"test": "data", "timestamp": datetime.now().isoformat()}
            success = atomic_json_write(test_file, data)
            print(f"  Write: {'OK' if success else 'FAILED'}")

            # Test safe JSON read
            print("Testing safe JSON read...")
            read_data = safe_json_read(test_file)
            print(f"  Read: {'OK' if read_data else 'FAILED'}")
            print(f"  Data: {read_data}")

            # Test atomic update
            print("Testing atomic update...")
            def add_field(d):
                d["updated"] = True
                return d
            success = atomic_update(test_file, add_field)
            print(f"  Update: {'OK' if success else 'FAILED'}")

            # Verify
            print("Verifying integrity...")
            integrity = verify_integrity(test_file)
            print(f"  Integrity: {integrity}")

            # Cleanup
            test_file.unlink()
            backup = test_file.with_suffix(".json.bak")
            if backup.exists():
                backup.unlink()

            print("\n[OK] All tests passed")

        elif cmd == "cleanup":
            directory = sys.argv[2] if len(sys.argv) > 2 else "."
            cleaned = cleanup_temp_files(directory)
            print(f"Cleaned {cleaned} temp files")

        elif cmd == "verify":
            if len(sys.argv) > 2:
                result = verify_integrity(sys.argv[2])
                print(json.dumps(result, indent=2))
            else:
                print("Usage: python atomic_io.py verify <file>")

        else:
            print(f"Unknown command: {cmd}")
            print("Usage: python atomic_io.py [test|cleanup|verify]")
    else:
        print("Genesis Atomic I/O")
        print("Usage: python atomic_io.py [test|cleanup <dir>|verify <file>]")
