"""
Security utilities for encryption, key management, and secure communications.
"""
import os
import base64
import hashlib
import time
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.x509 import load_pem_x509_certificate
from cryptography.x509.oid import NameOID
from cryptography import x509
from datetime import datetime, timedelta
import secrets
import uuid
import json
import datetime


class SecurityManager:
    """Manager for security operations including encryption, decryption, and cert management."""
    
    def __init__(self, certs_dir):
        """Initialize the security manager with the certificates directory."""
        self.certs_dir = certs_dir
        
    # Initialize keychain manager
        keychain_dir = os.path.join(os.path.dirname(certs_dir), 'keychain')
        os.makedirs(keychain_dir, exist_ok=True)
        self.keychain_manager = KeychainManager(self, keychain_dir)
        os.makedirs(certs_dir, exist_ok=True)
    
    def generate_rsa_keypair(self, key_size=4096):
        """Generate a new RSA key pair."""
        private_key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=key_size
        )
        public_key = private_key.public_key()
        
        return private_key, public_key
    
    def save_private_key(self, private_key, filename, password=None):
        """Save a private key to a file, optionally encrypted with a password."""
        encryption = serialization.NoEncryption()
        if password:
            encryption = serialization.BestAvailableEncryption(password.encode())
        
        pem = private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=encryption
        )
        
        with open(os.path.join(self.certs_dir, filename), 'wb') as f:
            f.write(pem)
    
    def save_public_key(self, public_key, filename):
        """Save a public key to a file."""
        pem = public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        )
        
        with open(os.path.join(self.certs_dir, filename), 'wb') as f:
            f.write(pem)
    
    def load_private_key(self, filename, password=None):
        """Load a private key from a file."""
        with open(os.path.join(self.certs_dir, filename), 'rb') as f:
            pem_data = f.read()
        
        password_bytes = password.encode() if password else None
        return serialization.load_pem_private_key(pem_data, password=password_bytes)
    
    def load_public_key(self, filename):
        """Load a public key from a file."""
        with open(os.path.join(self.certs_dir, filename), 'rb') as f:
            pem_data = f.read()
        
        return serialization.load_pem_public_key(pem_data)
    
    def generate_aes_key(self, key_size=256):
        """Generate a random AES key."""
        # Key size in bytes (256 bits = 32 bytes)
        key_bytes = key_size // 8
        return secrets.token_bytes(key_bytes)
    
    def encrypt_with_rsa(self, public_key, data):
        """Encrypt data using RSA public key."""
        if isinstance(data, str):
            data = data.encode()
        
        ciphertext = public_key.encrypt(
            data,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        
        return base64.b64encode(ciphertext).decode()
    
    def decrypt_with_rsa(self, private_key, encrypted_data):
        """Decrypt data using RSA private key."""
        if isinstance(encrypted_data, str):
            encrypted_data = base64.b64decode(encrypted_data)
        
        plaintext = private_key.decrypt(
            encrypted_data,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        
        return plaintext
    
    def encrypt_with_aes(self, key, data, iv=None):
        """Encrypt data using AES."""
        if isinstance(data, str):
            data = data.encode()
        
        if iv is None:
            iv = secrets.token_bytes(16)  # 16 bytes for AES
        
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
        encryptor = cipher.encryptor()
        
        # PKCS7 padding
        padder = self._pkcs7_pad(data, 16)
        padded_data = padder
        
        ciphertext = encryptor.update(padded_data) + encryptor.finalize()
        return iv + ciphertext  # Prepend IV to ciphertext
    
    def decrypt_with_aes(self, key, encrypted_data):
        """Decrypt data using AES."""
        iv = encrypted_data[:16]  # First 16 bytes are the IV
        ciphertext = encrypted_data[16:]  # The rest is the ciphertext
        
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
        decryptor = cipher.decryptor()
        
        padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
        plaintext = self._pkcs7_unpad(padded_plaintext)
        
        return plaintext
    
    def _pkcs7_pad(self, data, block_size):
        """PKCS#7 padding for data."""
        padding_length = block_size - (len(data) % block_size)
        padding = bytes([padding_length] * padding_length)
        return data + padding
    
    def _pkcs7_unpad(self, padded_data):
        """Remove PKCS#7 padding."""
        padding_length = padded_data[-1]
        return padded_data[:-padding_length]
    
    def generate_certificate(self, private_key, domain_name, expiry_days=365):
        """Generate a self-signed certificate for a domain."""
        subject = issuer = x509.Name([
            x509.NameAttribute(NameOID.COMMON_NAME, domain_name),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, "DecentraBrowser"),
        ])
        
        cert = x509.CertificateBuilder().subject_name(
            subject
        ).issuer_name(
            issuer
        ).public_key(
            private_key.public_key()
        ).serial_number(
            x509.random_serial_number()
        ).not_valid_before(
            datetime.utcnow()
        ).not_valid_after(
            datetime.utcnow() + timedelta(days=expiry_days)
        ).add_extension(
            x509.SubjectAlternativeName([x509.DNSName(domain_name)]),
            critical=False,
        ).sign(private_key, hashes.SHA256())
        
        cert_pem = cert.public_bytes(serialization.Encoding.PEM)
        cert_path = os.path.join(self.certs_dir, f"{domain_name}.cert")
        
        with open(cert_path, 'wb') as f:
            f.write(cert_pem)
        
        return cert_path
    
    def verify_certificate(self, cert_path, domain_name):
        """Verify a certificate's validity and domain name."""
        with open(cert_path, 'rb') as f:
            cert_data = f.read()
        
        cert = x509.load_pem_x509_certificate(cert_data)
        
        # Check expiration
        now = datetime.utcnow()
        if now < cert.not_valid_before or now > cert.not_valid_after:
            return False, "Certificate is expired or not yet valid"
        
        # Check domain
        try:
            ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
            domains = ext.value.get_values_for_type(x509.DNSName)
            if domain_name not in domains:
                return False, "Domain name doesn't match certificate"
        except x509.ExtensionNotFound:
            cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
            if domain_name != cn:
                return False, "Domain name doesn't match certificate"
        
        return True, "Certificate is valid"
    
    def get_certificate_fingerprint(self, cert_path):
        """Get the SHA-256 fingerprint of a certificate."""
        with open(cert_path, 'rb') as f:
            cert_data = f.read()
        
        cert = x509.load_pem_x509_certificate(cert_data)
        fingerprint = cert.fingerprint(hashes.SHA256())
        return ':'.join(format(b, '02x') for b in fingerprint)

    def pfs_init(self):
        """Initialize Perfect Forward Secrecy session."""
        # Generate ephemeral key pair for this session
        private_key, public_key = self.generate_rsa_keypair(key_size=2048)
        
        # Create a session ID
        session_id = secrets.token_hex(16)
        
        return {
            'session_id': session_id,
            'private_key': private_key,
            'public_key': public_key
        }
    
    def pfs_exchange(self, session, peer_public_key_data):
        """Exchange keys for PFS session."""
        # Load peer's ephemeral public key
        peer_public_key = serialization.load_pem_public_key(peer_public_key_data)
        
        # Generate session key
        shared_secret = secrets.token_bytes(32)  # 256-bit AES key
        
        # Encrypt session key with peer's public key
        encrypted_secret = peer_public_key.encrypt(
            shared_secret,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        
        # Update session
        session['shared_secret'] = shared_secret
        session['established'] = True
        
        return encrypted_secret
    
    def pfs_receive(self, session, encrypted_secret):
        """Receive and process peer's response in PFS session."""
        # Decrypt the session key using our ephemeral private key
        shared_secret = session['private_key'].decrypt(
            encrypted_secret,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        
        # Update session
        session['shared_secret'] = shared_secret
        session['established'] = True
        
        return True
    
    def pfs_encrypt(self, session, data):
        """Encrypt data using established PFS session."""
        if not session.get('established'):
            raise ValueError("PFS session not established")
        
        return self.encrypt_with_aes(session['shared_secret'], data)
    
    def pfs_decrypt(self, session, encrypted_data):
        """Decrypt data using established PFS session."""
        if not session.get('established'):
            raise ValueError("PFS session not established")
        
        return self.decrypt_with_aes(session['shared_secret'], encrypted_data)

class KeychainManager:
    
    
    def __init__(self, security_manager, keychain_dir):
        
        self.security_manager = security_manager
        self.keychain_dir = keychain_dir
        self.shared_keys_dir = os.path.join(keychain_dir, "shared")
        self.received_keys_dir = os.path.join(keychain_dir, "received")
        self.trusted_servers_dir = os.path.join(keychain_dir, "trusted_servers")
        
        # Create directories if they don't exist
        os.makedirs(self.shared_keys_dir, exist_ok=True)
        os.makedirs(self.received_keys_dir, exist_ok=True)
        os.makedirs(self.trusted_servers_dir, exist_ok=True)
        
        # Load trusted servers
        self.trusted_servers = self._load_trusted_servers()
    
    def _load_trusted_servers(self):
        
        trusted_servers = {}
        
        for file_name in os.listdir(self.trusted_servers_dir):
            if file_name.endswith('.pub'):
                server_name = file_name[:-4]  # Remove .pub extension
                key_path = os.path.join(self.trusted_servers_dir, file_name)
                
                try:
                    with open(key_path, 'rb') as f:
                        public_key_data = f.read()
                    
                    # Load public key
                    public_key = serialization.load_pem_public_key(public_key_data)
                    trusted_servers[server_name] = {
                        'public_key': public_key,
                        'public_key_pem': public_key_data
                    }
                except Exception as e:
                    print(f"Error loading trusted server key {file_name}: {str(e)}")
        
        return trusted_servers
    
    def add_trusted_server(self, server_name, public_key_pem):
        
        try:
            # Validate the public key
            public_key = serialization.load_pem_public_key(public_key_pem)
            
            # Save the public key
            key_path = os.path.join(self.trusted_servers_dir, f"{server_name}.pub")
            with open(key_path, 'wb') as f:
                f.write(public_key_pem)
            
            # Add to trusted servers
            self.trusted_servers[server_name] = {
                'public_key': public_key,
                'public_key_pem': public_key_pem
            }
            
            return True, "Server added to trusted list"
        except Exception as e:
            return False, f"Error adding trusted server: {str(e)}"
    
    def remove_trusted_server(self, server_name):
        
        if server_name in self.trusted_servers:
            try:
                # Remove the public key file
                key_path = os.path.join(self.trusted_servers_dir, f"{server_name}.pub")
                if os.path.exists(key_path):
                    os.remove(key_path)
                
                # Remove from trusted servers
                del self.trusted_servers[server_name]
                
                return True, "Server removed from trusted list"
            except Exception as e:
                return False, f"Error removing trusted server: {str(e)}"
        else:
            return False, "Server not found in trusted list"
    
    def list_trusted_servers(self):
        
        return list(self.trusted_servers.keys())
    
    def store_key_for_file(self, file_path, aes_key, recipients=None):
        
        try:
            # Generate a unique key ID
            key_id = str(uuid.uuid4())
            
            # Create key metadata
            metadata = {
                'key_id': key_id,
                'file_path': os.path.basename(file_path),
                'created': datetime.datetime.now().isoformat(),
                'recipients': []
            }
            
            # Store the key in the shared keys directory
            key_path = os.path.join(self.shared_keys_dir, f"{key_id}.key")
            with open(key_path, 'wb') as f:
                f.write(aes_key)
            
            # Store metadata
            metadata_path = os.path.join(self.shared_keys_dir, f"{key_id}.meta")
            with open(metadata_path, 'w') as f:
                json.dump(metadata, f, indent=2)
            
            # If recipients specified, encrypt the key for each recipient
            if recipients:
                for recipient in recipients:
                    if recipient in self.trusted_servers:
                        server = self.trusted_servers[recipient]
                        public_key = server['public_key']
                        
                        # Encrypt the AES key with the recipient's public key
                        encrypted_key = self.security_manager.encrypt_with_rsa(public_key, aes_key)
                        
                        # Store the encrypted key
                        recipient_key_path = os.path.join(
                            self.shared_keys_dir, 
                            f"{key_id}.{recipient}.enc"
                        )
                        with open(recipient_key_path, 'w') as f:
                            f.write(encrypted_key)
                        
                        # Update metadata
                        metadata['recipients'].append(recipient)
                
                # Update metadata file with recipients
                with open(metadata_path, 'w') as f:
                    json.dump(metadata, f, indent=2)
            
            return True, key_id
        except Exception as e:
            return False, f"Error storing key: {str(e)}"
    
    def get_key_for_file(self, file_path):
        
        try:
            file_name = os.path.basename(file_path)
            
            # Search in shared keys
            for meta_file in os.listdir(self.shared_keys_dir):
                if meta_file.endswith('.meta'):
                    meta_path = os.path.join(self.shared_keys_dir, meta_file)
                    with open(meta_path, 'r') as f:
                        metadata = json.load(f)
                    
                    if metadata['file_path'] == file_name:
                        key_id = metadata['key_id']
                        key_path = os.path.join(self.shared_keys_dir, f"{key_id}.key")
                        
                        if os.path.exists(key_path):
                            with open(key_path, 'rb') as f:
                                return True, f.read()
            
            # Search in received keys
            for meta_file in os.listdir(self.received_keys_dir):
                if meta_file.endswith('.meta'):
                    meta_path = os.path.join(self.received_keys_dir, meta_file)
                    with open(meta_path, 'r') as f:
                        metadata = json.load(f)
                    
                    if metadata['file_path'] == file_name:
                        key_id = metadata['key_id']
                        key_path = os.path.join(self.received_keys_dir, f"{key_id}.key")
                        
                        if os.path.exists(key_path):
                            with open(key_path, 'rb') as f:
                                return True, f.read()
            
            return False, "Key not found for file"
        except Exception as e:
            return False, f"Error retrieving key: {str(e)}"
    
    def receive_encrypted_key(self, encrypted_key_data, sender, file_path):
        
        try:
            # Generate a key ID
            key_id = str(uuid.uuid4())
            
            # Load our private key
            private_key = self.security_manager.load_private_key('private_key.pem')
            
            # Decode if base64 encoded
            if isinstance(encrypted_key_data, str):
                encrypted_key_data = base64.b64decode(encrypted_key_data)
            
            # Decrypt the key
            decrypted_key = self.security_manager.decrypt_with_rsa(private_key, encrypted_key_data)
            
            # Store the decrypted key
            key_path = os.path.join(self.received_keys_dir, f"{key_id}.key")
            with open(key_path, 'wb') as f:
                f.write(decrypted_key)
            
            # Create metadata
            metadata = {
                'key_id': key_id,
                'file_path': os.path.basename(file_path),
                'received_from': sender,
                'received': datetime.datetime.now().isoformat()
            }
            
            # Store metadata
            metadata_path = os.path.join(self.received_keys_dir, f"{key_id}.meta")
            with open(metadata_path, 'w') as f:
                json.dump(metadata, f, indent=2)
            
            return True, key_id
        except Exception as e:
            return False, f"Error receiving encrypted key: {str(e)}"
    
    def export_encrypted_key(self, key_id, recipient):
        
        try:
            # Check if recipient is trusted
            if recipient not in self.trusted_servers:
                return False, "Recipient not in trusted servers list"
            
            # Find the key
            key_path = os.path.join(self.shared_keys_dir, f"{key_id}.key")
            if not os.path.exists(key_path):
                return False, "Key not found"
            
            # Check if already encrypted for this recipient
            recipient_key_path = os.path.join(self.shared_keys_dir, f"{key_id}.{recipient}.enc")
            if os.path.exists(recipient_key_path):
                # Return existing encrypted key
                with open(recipient_key_path, 'r') as f:
                    return True, f.read()
            
            # Load the key
            with open(key_path, 'rb') as f:
                aes_key = f.read()
            
            # Get recipient's public key
            public_key = self.trusted_servers[recipient]['public_key']
            
            # Encrypt the key
            encrypted_key = self.security_manager.encrypt_with_rsa(public_key, aes_key)
            
            # Store the encrypted key
            with open(recipient_key_path, 'w') as f:
                f.write(encrypted_key)
            
            # Update metadata
            meta_path = os.path.join(self.shared_keys_dir, f"{key_id}.meta")
            if os.path.exists(meta_path):
                with open(meta_path, 'r') as f:
                    metadata = json.load(f)
                
                if recipient not in metadata['recipients']:
                    metadata['recipients'].append(recipient)
                
                with open(meta_path, 'w') as f:
                    json.dump(metadata, f, indent=2)
            
            return True, encrypted_key
        except Exception as e:
            return False, f"Error exporting encrypted key: {str(e)}"
    
    def delete_key(self, key_id, received=False):
        
        try:
            # Determine the directory
            key_dir = self.received_keys_dir if received else self.shared_keys_dir
            
            # Delete key files
            key_path = os.path.join(key_dir, f"{key_id}.key")
            if os.path.exists(key_path):
                os.remove(key_path)
            
            meta_path = os.path.join(key_dir, f"{key_id}.meta")
            if os.path.exists(meta_path):
                os.remove(meta_path)
            
            # Delete encrypted keys for recipients
            if not received:
                for file_name in os.listdir(key_dir):
                    if file_name.startswith(f"{key_id}.") and file_name.endswith(".enc"):
                        os.remove(os.path.join(key_dir, file_name))
            
            return True, "Key deleted successfully"
        except Exception as e:
            return False, f"Error deleting key: {str(e)}"
    
    def list_keys(self, received=False):
        
        keys = []
        key_dir = self.received_keys_dir if received else self.shared_keys_dir
        
        for file_name in os.listdir(key_dir):
            if file_name.endswith('.meta'):
                meta_path = os.path.join(key_dir, file_name)
                try:
                    with open(meta_path, 'r') as f:
                        metadata = json.load(f)
                    keys.append(metadata)
                except Exception as e:
                    print(f"Error loading metadata {file_name}: {str(e)}")
        
        return keys
