"""
This module provides the core encryption and decryption engine for File Vault.
It handles individual file processing and batch folder operations, ensuring
that files are modified in place using atomic write operations.
"""

import os
import tempfile
import pathlib
from typing import List, Dict, Tuple, Union
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag

from filevault.core import file_detector, key_manager

# Magic marker for the file header
MAGIC_MARKER = b"FVLT"
NONCE_SIZE = 12
SALT_SIZE = 16


def _safe_write(filepath: str, data: bytes) -> None:
    """
    Writes data to a temporary file and then replaces the original file
    atomically. This prevents file corruption if the process is interrupted.

    Args:
        filepath (str): The destination file path.
        data (bytes): The bytes to write.
    """
    path = pathlib.Path(filepath)
    # Create temp file in the same directory to ensure it's on the same filesystem for os.replace
    fd, temp_path = tempfile.mkstemp(dir=path.parent, prefix=".fv_tmp_")
    
    try:
        with os.fdopen(fd, 'wb') as tmp:
            tmp.write(data)
            tmp.flush()
            os.fsync(tmp.fileno())
        os.replace(temp_path, filepath)
    except Exception:
        if os.path.exists(temp_path):
            os.remove(temp_path)
        raise


def encrypt_file(filepath: str, password: str) -> str:
    """
    Encrypts a file in place using AES-256-GCM.

    Args:
        filepath (str): Path to the file to encrypt.
        password (str): The encryption password.

    Returns:
        str: The path to the encrypted file.

    Raises:
        ValueError: If the file is unsupported or already encrypted.
    """
    # Step 1: Check support
    supported, reason = file_detector.is_supported(filepath)
    if not supported:
        raise ValueError(reason)

    # Step 2: Check if already encrypted
    if file_detector.is_encrypted(filepath):
        raise ValueError("This file is already encrypted. Decrypt it first.")

    # Step 3: Read content
    with open(filepath, "rb") as f:
        plaintext = f.read()

    # Step 4-6: Setup keys and nonces
    salt = key_manager.generate_salt()
    nonce = os.urandom(NONCE_SIZE)
    key = key_manager.derive_key(password, salt)

    # Step 7: Encrypt
    aesgcm = AESGCM(key)
    # AESGCM.encrypt returns ciphertext + tag
    ciphertext = aesgcm.encrypt(nonce, plaintext, None)

    # Step 8: Construct layout and write back
    # [4 bytes marker][16 bytes salt][12 bytes nonce][ciphertext]
    final_data = MAGIC_MARKER + salt + nonce + ciphertext
    _safe_write(filepath, final_data)

    return filepath


def decrypt_file(filepath: str, password: str) -> str:
    """
    Decrypts a file in place using AES-256-GCM.

    Args:
        filepath (str): Path to the file to decrypt.
        password (str): The decryption password.

    Returns:
        str: The path to the decrypted file.

    Raises:
        ValueError: If the file is not encrypted or the password is wrong.
    """
    # Step 1: Check if encrypted
    if not file_detector.is_encrypted(filepath):
        raise ValueError("This file is not encrypted.")

    # Step 2: Read content
    with open(filepath, "rb") as f:
        content = f.read()

    # Step 3: Parse layout
    # content[0:4] is MAGIC_MARKER
    salt = content[4:20]
    nonce = content[20:32]
    ciphertext = content[32:]

    # Step 4: Derive key
    key = key_manager.derive_key(password, salt)

    # Step 5: Decrypt
    aesgcm = AESGCM(key)
    try:
        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
    except InvalidTag:
        raise ValueError("Wrong password. The file has not been modified.")

    # Step 6: Write back
    _safe_write(filepath, plaintext)

    return filepath


def encrypt_folder(folder_path: str, password: str) -> Dict[str, Union[List[str], List[Tuple[str, str]]]]:
    """
    Encrypts all supported files within a folder.

    Args:
        folder_path (str): The path to the directory.
        password (str): The encryption password.

    Returns:
        dict: Summary of the operation with 'encrypted', 'skipped', and 'errors'.
    """
    results = {
        "encrypted": [],
        "skipped": [],
        "errors": []
    }
    
    folder = pathlib.Path(folder_path)
    if not folder.is_dir():
        results["errors"].append((str(folder), "Not a directory"))
        return results

    for item in folder.iterdir():
        if not item.is_file():
            continue

        item_str = str(item)
        
        # Check if supported
        supported, _ = file_detector.is_supported(item_str)
        if not supported:
            results["skipped"].append(item.name)
            continue

        # Check if already encrypted
        if file_detector.is_encrypted(item_str):
            results["skipped"].append(item.name)
            continue

        try:
            encrypt_file(item_str, password)
            results["encrypted"].append(item.name)
        except Exception as e:
            results["errors"].append((item.name, str(e)))

    return results


def decrypt_folder(folder_path: str, password: str) -> Dict[str, Union[List[str], List[Tuple[str, str]]]]:
    """
    Decrypts all encrypted files within a folder.

    Args:
        folder_path (str): The path to the directory.
        password (str): The decryption password.

    Returns:
        dict: Summary of the operation with 'decrypted', 'skipped', and 'errors'.
    """
    results = {
        "decrypted": [],
        "skipped": [],
        "errors": []
    }

    folder = pathlib.Path(folder_path)
    if not folder.is_dir():
        results["errors"].append((str(folder), "Not a directory"))
        return results

    for item in folder.iterdir():
        if not item.is_file():
            continue

        item_str = str(item)

        # Only process encrypted files
        if not file_detector.is_encrypted(item_str):
            results["skipped"].append(item.name)
            continue

        try:
            decrypt_file(item_str, password)
            results["decrypted"].append(item.name)
        except Exception as e:
            results["errors"].append((item.name, str(e)))

    return results
