"""
File storage layer for Kilit.

This module handles all file IO operations including atomic writes,
file permissions, and cross-platform compatibility.
"""

from __future__ import annotations

import base64
import json
import os
import platform
import tempfile
from pathlib import Path

from kilit.exceptions import KilitFileError


def set_secure_permissions(path: Path) -> bool:
    """
    Set secure file permissions (0o600 on POSIX, best-effort on Windows).

    Args:
        path: Path to file

    Returns:
        True if permissions were set successfully, False otherwise

    Platform Behavior:
    - POSIX (Linux, macOS, Unix): Sets mode to 0o600 (owner read/write only)
    - Windows: Attempts os.chmod, logs warning if fails, returns False

    Security:
    - 0o600 means only the file owner can read/write
    - Other users and groups have no access
    - This protects master keys and temporary files from unauthorized access

    Cross-Platform Notes:
    - Windows file permissions work differently than POSIX
    - os.chmod on Windows may not have the same effect
    - We use best-effort approach: try to set, don't crash if it fails
    - Windows users should rely on NTFS ACLs for proper security
    """
    try:
        os.chmod(path, 0o600)
        return True
    except (OSError, NotImplementedError, FileNotFoundError):
        # On Windows, chmod may not work as expected or may raise NotImplementedError
        # FileNotFoundError means file doesn't exist
        # Log warning but don't crash
        if platform.system() == "Windows":
            # Best-effort on Windows - permissions work differently
            return False
        else:
            # On POSIX systems, this is a real error
            raise


def check_permissions(path: Path) -> bool:
    """
    Check if file has secure permissions (0o600 on POSIX, always True on Windows).

    Args:
        path: Path to file

    Returns:
        True if permissions are secure (or on Windows), False otherwise

    Platform Behavior:
    - POSIX: Checks if mode is exactly 0o600
    - Windows: Always returns True (best-effort, permissions work differently)

    Note:
        This is a best-effort check. On Windows, NTFS ACLs should be used
        for proper security, which is beyond the scope of this check.
    """
    if platform.system() == "Windows":
        # On Windows, we can't reliably check POSIX-style permissions
        # Return True (best-effort)
        return True

    try:
        stat_info = os.stat(path)
        mode = stat_info.st_mode & 0o777  # Get permission bits
        return mode == 0o600
    except OSError:
        return False


def atomic_write(path: Path, content: str) -> None:
    """
    Write content to file atomically with fsync for durability.

    Args:
        path: Target file path
        content: String content to write

    Raises:
        KilitFileError: If write fails (disk full, permissions, etc.)

    Process:
    1. Create temp file in same directory as target (required for os.replace atomicity)
    2. Write content to temp file
    3. Call os.fsync() to ensure data is written to disk
    4. Close temp file
    5. Set secure permissions on temp file
    6. Use os.replace() to atomically move temp to target
    7. Clean up temp file in finally block if error occurs

    Atomicity Guarantees:
    - os.replace() is atomic on all platforms (POSIX and Windows)
    - Either the operation completes fully or the original file is unchanged
    - No partial writes or corrupted files
    - Temp file must be in same directory for atomicity (same filesystem)

    Durability:
    - os.fsync() ensures data is written to physical disk
    - Protects against power loss or system crash
    - May have performance impact but ensures data safety

    Error Handling:
    - Disk full: Raises KilitFileError, temp file cleaned up
    - Permission denied: Raises KilitFileError, temp file cleaned up
    - Interrupted write: Temp file cleaned up in finally block
    - Original file never modified if error occurs

    Cross-Platform:
    - Works on POSIX (Linux, macOS, Unix) and Windows
    - os.replace() is atomic on all platforms
    - Temp file in same directory ensures same filesystem
    """
    # Ensure parent directory exists
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    # Create temp file in same directory (required for atomic os.replace)
    temp_fd = None
    temp_path = None

    try:
        # Create temp file with delete=False (we'll handle deletion)
        temp_fd, temp_path_str = tempfile.mkstemp(
            dir=path.parent, prefix=".kilit_tmp_", suffix=".tmp", text=True
        )
        temp_path = Path(temp_path_str)

        # Write content to temp file
        try:
            os.write(temp_fd, content.encode("utf-8"))
        except OSError as e:
            raise KilitFileError(f"Failed to write to temp file: {e}")

        # Fsync to ensure data is written to disk
        try:
            os.fsync(temp_fd)
        except OSError:
            # Fsync may not be supported on all filesystems, but try anyway
            pass

        # Close temp file
        os.close(temp_fd)
        temp_fd = None  # Mark as closed

        # Set secure permissions on temp file
        try:
            set_secure_permissions(temp_path)
        except OSError as e:
            raise KilitFileError(f"Failed to set permissions on temp file: {e}")

        # Atomically replace target file with temp file
        try:
            os.replace(temp_path, path)
            temp_path = None  # Mark as moved (no cleanup needed)
        except OSError as e:
            raise KilitFileError(f"Failed to replace file atomically: {e}")

    except KilitFileError:
        # Re-raise KilitFileError as-is
        raise
    except Exception as e:
        # Wrap unexpected errors
        raise KilitFileError(f"Unexpected error during atomic write: {e}")
    finally:
        # Clean up temp file if it still exists
        if temp_fd is not None:
            try:
                os.close(temp_fd)
            except OSError:
                pass

        if temp_path is not None and temp_path.exists():
            try:
                temp_path.unlink()
            except OSError:
                pass


class FileStorage:
    """
    File storage operations for Kilit.

    This class handles all file IO for master keys and encrypted envelopes.
    All operations use atomic writes and secure permissions.

    Methods:
    - save_key: Save master key to file with secure permissions
    - load_key: Load and validate master key from file
    - save_envelope: Save encrypted envelope to file
    - load_envelope: Load and parse encrypted envelope from file

    Error Handling:
    - All methods raise KilitFileError on IO failures
    - Clear error messages indicate the specific failure
    """

    def save_key(self, key: str, path: Path) -> None:
        """
        Save master key to file with secure permissions.

        Args:
            key: Base64 urlsafe encoded master key
            path: Path to save key file

        Raises:
            KilitFileError: If save fails

        Process:
        1. Use atomic_write to save key
        2. Secure permissions set automatically by atomic_write
        3. File is created with 0o600 on POSIX, best-effort on Windows
        """
        try:
            atomic_write(path, key)
        except KilitFileError:
            raise
        except Exception as e:
            raise KilitFileError(f"Failed to save master key to {path}: {e}")

    def load_key(self, path: Path) -> str:
        """
        Load and validate master key from file.

        Args:
            path: Path to key file

        Returns:
            Base64 urlsafe encoded master key

        Raises:
            KilitFileError: If file not found or invalid format

        Validation:
        - Checks file exists
        - Reads content
        - Validates base64 format
        - Returns key string
        """
        try:
            path = Path(path)
            if not path.exists():
                raise KilitFileError(f"Master key file not found: {path}")

            key = path.read_text().strip()

            # Validate base64 format
            try:
                # Add padding if needed
                padding = "=" * (4 - len(key) % 4) if len(key) % 4 else ""
                base64.urlsafe_b64decode(key + padding)
            except Exception as e:
                raise KilitFileError(f"Invalid master key format in {path}: {e}")

            return key

        except KilitFileError:
            raise
        except Exception as e:
            raise KilitFileError(f"Failed to load master key from {path}: {e}")

    def save_envelope(self, envelope: dict, path: Path) -> None:
        """
        Save encrypted envelope to file.

        Args:
            envelope: Envelope dictionary
            path: Path to save envelope file

        Raises:
            KilitFileError: If save fails

        Process:
        1. Serialize envelope to JSON (pretty-printed)
        2. Use atomic_write to save
        3. Secure permissions set automatically
        """
        try:
            # Serialize to JSON with pretty printing
            json_content = json.dumps(envelope, indent=2, sort_keys=True)
            atomic_write(path, json_content)
        except KilitFileError:
            raise
        except Exception as e:
            raise KilitFileError(f"Failed to save envelope to {path}: {e}")

    def load_envelope(self, path: Path) -> dict:
        """
        Load and parse encrypted envelope from file.

        Args:
            path: Path to envelope file

        Returns:
            Envelope dictionary

        Raises:
            KilitFileError: If file not found or invalid JSON

        Process:
        1. Check file exists
        2. Read content
        3. Parse JSON
        4. Return envelope dict
        """
        try:
            path = Path(path)
            if not path.exists():
                raise KilitFileError(f"Encrypted file not found: {path}")

            content = path.read_text()

            # Parse JSON
            try:
                envelope = json.loads(content)
            except json.JSONDecodeError as e:
                raise KilitFileError(f"Invalid JSON in {path}: {e}")

            return envelope

        except KilitFileError:
            raise
        except Exception as e:
            raise KilitFileError(f"Failed to load envelope from {path}: {e}")
