"""
Stegasoo Cryptographic Functions (v4.0.0 - Channel Key Support)

This is the crypto layer - where we turn plaintext into indecipherable noise.

The security model is multi-factor:
┌────────────────────────────────────────────────────────────────────┐
│  SOMETHING YOU HAVE          SOMETHING YOU KNOW                    │
│  ├─ Reference photo          ├─ Passphrase (4+ BIP-39 words)      │
│  └─ RSA private key (opt)    └─ PIN (6-9 digits)                  │
│                                                                    │
│  DEPLOYMENT BINDING                                                │
│  └─ Channel key (ties messages to a specific server/group)        │
└────────────────────────────────────────────────────────────────────┘

All factors get mixed together through Argon2id (memory-hard KDF) to derive
the actual encryption key. Miss any factor = wrong key = garbage output.

Encryption: AES-256-GCM (authenticated encryption - tamper = detection)
KDF: Argon2id (256MB RAM, 4 iterations) or PBKDF2 fallback (600K iterations)

v4.0.0: Added channel key for server/group isolation
v3.2.0: Removed date dependency (was cute but annoying in practice)
"""

import hashlib
import io
import secrets
import struct

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes as _hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.hkdf import HKDFExpand
from PIL import Image

from .constants import (
    ARGON2_MEMORY_COST,
    ARGON2_PARALLELISM,
    ARGON2_TIME_COST,
    FORMAT_VERSION,
    FORMAT_VERSION_LEGACY,
    HKDF_INFO_ENCRYPT,
    IV_SIZE,
    MAGIC_HEADER,
    MAX_FILENAME_LENGTH,
    MESSAGE_NONCE_SIZE,
    PAYLOAD_FILE,
    PAYLOAD_TEXT,
    PBKDF2_ITERATIONS,
    SALT_SIZE,
    TAG_SIZE,
)
from .debug import get_logger
from .exceptions import DecryptionError, EncryptionError, InvalidHeaderError, KeyDerivationError
from .models import DecodeResult, FilePayload

logger = get_logger(__name__)

# Check for Argon2 availability
try:
    from argon2.low_level import Type, hash_secret_raw

    HAS_ARGON2 = True
except ImportError:
    HAS_ARGON2 = False
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC



# =============================================================================
# CHANNEL KEY RESOLUTION
# =============================================================================

# Sentinel value for "use auto-detected channel key"
CHANNEL_KEY_AUTO = "auto"


def _resolve_channel_key(channel_key: str | bool | None) -> bytes | None:
    """
    Resolve channel key parameter to actual key hash.

    Args:
        channel_key: Channel key parameter with these behaviors:
            - None or "auto": Use server's configured key (from env/config)
            - str (valid key): Use this specific key
            - "" or False: Explicitly use NO channel key (public mode)

    Returns:
        32-byte channel key hash, or None for public mode
    """
    # Explicit public mode
    if channel_key == "" or channel_key is False:
        return None

    # Auto-detect from environment/config
    if channel_key is None or channel_key == CHANNEL_KEY_AUTO:
        from .channel import get_channel_key_hash

        return get_channel_key_hash()

    # Explicit key provided - validate and hash it
    if isinstance(channel_key, str):
        from .channel import format_channel_key, validate_channel_key

        if not validate_channel_key(channel_key):
            raise ValueError(f"Invalid channel key format: {channel_key}")
        formatted = format_channel_key(channel_key)
        return hashlib.sha256(formatted.encode("utf-8")).digest()

    raise ValueError(f"Invalid channel_key type: {type(channel_key)}")


# =============================================================================
# CORE CRYPTO FUNCTIONS
# =============================================================================
#
# The "reference photo as a key" concept is one of Stegasoo's unique features.
# Most steganography tools just use a password. We add the photo as a
# "something you have" factor - like a hardware token, but it's a cat picture.


def hash_photo(image_data: bytes) -> bytes:
    """
    Compute deterministic hash of photo pixel content.

    This is the magic sauce that turns your cat photo into a cryptographic key.

    Why pixels and not the file hash?
    - File metadata changes (EXIF stripped, resaved) = different file hash
    - But pixel content stays the same
    - We hash the RGB values directly, so format conversions don't matter

    The double-hash with prefix is belt-and-suspenders mixing. Probably
    overkill, but hey, it's crypto - paranoia is a feature.

    Args:
        image_data: Raw image file bytes (any format PIL can read)

    Returns:
        32-byte SHA-256 hash of pixel content
    """
    # Convert to RGB to normalize (RGBA, grayscale, etc. all become RGB)
    img: Image.Image = Image.open(io.BytesIO(image_data)).convert("RGB")
    pixels = img.tobytes()

    # Double-hash: SHA256(SHA256(pixels) + first 1KB of pixels)
    # The prefix adds image-specific data to prevent length-extension shenanigans
    h = hashlib.sha256(pixels).digest()
    h = hashlib.sha256(h + pixels[:1024]).digest()
    return h


def derive_hybrid_key(
    photo_data: bytes,
    passphrase: str,
    salt: bytes,
    pin: str = "",
    rsa_key_data: bytes | None = None,
    channel_key: str | bool | None = None,
) -> bytes:
    """
    Derive encryption key from multiple factors.

    This is the heart of Stegasoo's security model. We take all the things
    you need to prove you're authorized (photo, passphrase, PIN, etc.) and
    blend them together into one 32-byte key.

    The flow:
    ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
    │ Photo hash  │ + │ passphrase  │ + │ PIN + RSA   │ + salt
    └─────────────┘   └─────────────┘   └─────────────┘
           │                 │                 │
           └────────────────┴────────────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │    Argon2id     │  <- Memory-hard KDF
                    │   256MB / 4 iter │  <- Makes brute force expensive
                    └─────────────────┘
                             │
                             ▼
                      32-byte AES key

    Why Argon2id?
    - Memory-hard: attackers can't just throw GPUs at it
    - 256MB RAM per attempt = expensive at scale
    - Winner of the Password Hashing Competition (2015)
    - "id" variant resists both side-channel and GPU attacks

    Fallback: PBKDF2-SHA512 with 600K iterations (for systems without argon2)

    Args:
        photo_data: Reference photo bytes
        passphrase: Shared passphrase (recommend 4+ words from BIP-39)
        salt: Random salt for this message (32 bytes)
        pin: Optional static PIN
        rsa_key_data: Optional RSA key bytes
        channel_key: Channel key parameter:
            - None or "auto": Use configured key
            - str: Use this specific key
            - "" or False: No channel key (public mode)

    Returns:
        32-byte derived key (ready for AES-256)

    Raises:
        KeyDerivationError: If key derivation fails
    """
    try:
        photo_hash = hash_photo(photo_data)
        logger.debug(
            "derive_hybrid_key: photo_hash=%s, pin=%s, rsa=%s, channel=%s, salt=%d bytes",
            photo_hash[:4].hex(),
            "set" if pin else "none",
            "set" if rsa_key_data else "none",
            (
                "explicit"
                if isinstance(channel_key, str) and channel_key
                else "auto" if channel_key is None else "none"
            ),
            len(salt),
        )

        # Resolve channel key (server-specific binding)
        channel_hash = _resolve_channel_key(channel_key)

        # Build key material by concatenating all factors
        # Passphrase is lowercased to be forgiving of case differences
        key_material = photo_hash + passphrase.lower().encode() + pin.encode() + salt

        # Add RSA key hash if provided (another "something you have")
        if rsa_key_data:
            key_material += hashlib.sha256(rsa_key_data).digest()

        # Add channel key hash if configured (v4.0.0 - deployment binding)
        if channel_hash:
            key_material += channel_hash

        logger.debug("Key material: %d bytes", len(key_material))

        # Run it all through the KDF
        if HAS_ARGON2:
            logger.debug(
                "KDF: Argon2id (memory=%dKB, time=%d, parallel=%d)",
                ARGON2_MEMORY_COST,
                ARGON2_TIME_COST,
                ARGON2_PARALLELISM,
            )
            # Argon2id: the good stuff
            key = hash_secret_raw(
                secret=key_material,
                salt=salt[:32],
                time_cost=ARGON2_TIME_COST,  # 4 iterations
                memory_cost=ARGON2_MEMORY_COST,  # 256 MB RAM
                parallelism=ARGON2_PARALLELISM,  # 4 threads
                hash_len=32,
                type=Type.ID,  # Hybrid mode: resists side-channel AND GPU attacks
            )
        else:
            logger.warning(
                "KDF: PBKDF2 fallback (%d iterations) - argon2 not available", PBKDF2_ITERATIONS
            )
            # PBKDF2 fallback for systems without argon2-cffi
            # 600K iterations is slow but not memory-hard
            kdf = PBKDF2HMAC(
                algorithm=hashes.SHA512(),
                length=32,
                salt=salt,
                iterations=PBKDF2_ITERATIONS,
                backend=default_backend(),
            )
            key = kdf.derive(key_material)

        logger.debug("KDF complete, derived %d-byte key", len(key))
        return key

    except Exception as e:
        raise KeyDerivationError(f"Failed to derive key: {e}") from e


def derive_pixel_key(
    photo_data: bytes,
    passphrase: str,
    pin: str = "",
    rsa_key_data: bytes | None = None,
    channel_key: str | bool | None = None,
) -> bytes:
    """
    Derive key for pseudo-random pixel selection.

    This key determines which pixels are used for embedding,
    making the message location unpredictable without the correct inputs.

    Args:
        photo_data: Reference photo bytes
        passphrase: Shared passphrase
        pin: Optional static PIN
        rsa_key_data: Optional RSA key bytes
        channel_key: Channel key parameter (see derive_hybrid_key)

    Returns:
        32-byte key for pixel selection
    """
    photo_hash = hash_photo(photo_data)

    # Resolve channel key
    channel_hash = _resolve_channel_key(channel_key)

    material = photo_hash + passphrase.lower().encode() + pin.encode()

    if rsa_key_data:
        material += hashlib.sha256(rsa_key_data).digest()

    # Add channel key hash if configured (v4.0.0)
    if channel_hash:
        material += channel_hash

    return hashlib.sha256(material + b"pixel_selection").digest()


def derive_message_key(root_key: bytes, nonce: bytes) -> bytes:
    """
    Derive a per-message encryption key via HKDF-Expand.

    Each message gets a unique encryption key even with identical credentials,
    because the nonce is random per message. This provides key diversification:
    compromising the ciphertext of one message doesn't help with another.

    Args:
        root_key: 32-byte root key from Argon2id/PBKDF2
        nonce: 16-byte random nonce (unique per message)

    Returns:
        32-byte per-message encryption key
    """
    hkdf = HKDFExpand(
        algorithm=_hashes.SHA256(),
        length=32,
        info=HKDF_INFO_ENCRYPT + nonce,
        backend=default_backend(),
    )
    return hkdf.derive(root_key)


def _pack_payload(
    content: str | bytes | FilePayload,
) -> tuple[bytes, int]:
    """
    Pack payload with type marker and metadata.

    Format for text:
        [type:1][data]

    Format for file:
        [type:1][filename_len:2][filename][mime_len:2][mime][data]

    Args:
        content: Text string, raw bytes, or FilePayload

    Returns:
        Tuple of (packed bytes, payload type)
    """
    if isinstance(content, str):
        # Text message
        data = content.encode("utf-8")
        return bytes([PAYLOAD_TEXT]) + data, PAYLOAD_TEXT

    elif isinstance(content, FilePayload):
        # File with metadata
        filename = content.filename[:MAX_FILENAME_LENGTH].encode("utf-8")
        mime = (content.mime_type or "")[:100].encode("utf-8")

        packed = (
            bytes([PAYLOAD_FILE])
            + struct.pack(">H", len(filename))
            + filename
            + struct.pack(">H", len(mime))
            + mime
            + content.data
        )
        return packed, PAYLOAD_FILE

    else:
        # Raw bytes - treat as file with no name
        packed = (
            bytes([PAYLOAD_FILE])
            + struct.pack(">H", 0)  # No filename
            + struct.pack(">H", 0)  # No mime
            + content
        )
        return packed, PAYLOAD_FILE


def _unpack_payload(data: bytes) -> DecodeResult:
    """
    Unpack payload and extract content with metadata.

    Args:
        data: Packed payload bytes

    Returns:
        DecodeResult with appropriate content
    """
    if len(data) < 1:
        raise DecryptionError("Empty payload")

    payload_type = data[0]

    if payload_type == PAYLOAD_TEXT:
        # Text message
        text = data[1:].decode("utf-8")
        return DecodeResult(payload_type="text", message=text)

    elif payload_type == PAYLOAD_FILE:
        # File with metadata
        offset = 1

        # Read filename
        filename_len = struct.unpack(">H", data[offset : offset + 2])[0]
        offset += 2
        filename = data[offset : offset + filename_len].decode("utf-8") if filename_len else None
        offset += filename_len

        # Read mime type
        mime_len = struct.unpack(">H", data[offset : offset + 2])[0]
        offset += 2
        mime_type = data[offset : offset + mime_len].decode("utf-8") if mime_len else None
        offset += mime_len

        # Rest is file data
        file_data = data[offset:]

        return DecodeResult(
            payload_type="file", file_data=file_data, filename=filename, mime_type=mime_type
        )

    else:
        # Unknown type - try to decode as text (backward compatibility)
        try:
            text = data.decode("utf-8")
            return DecodeResult(payload_type="text", message=text)
        except UnicodeDecodeError:
            return DecodeResult(payload_type="file", file_data=data)


# =============================================================================
# HEADER FLAGS (v4.0.0)
# =============================================================================
#
# The flags byte tells us about the message without decrypting it.
# Currently just one flag, but the byte gives us room for 8.

FLAG_CHANNEL_KEY = 0x01  # Bit 0: Message was encoded with a channel key
# Future flags could include: compression, file attachment, etc.


def encrypt_message(
    message: str | bytes | FilePayload,
    photo_data: bytes,
    passphrase: str,
    pin: str = "",
    rsa_key_data: bytes | None = None,
    channel_key: str | bool | None = None,
) -> bytes:
    """
    Encrypt message or file using AES-256-GCM.

    This is where plaintext becomes ciphertext. We use AES-256-GCM which is:
    - AES: The standard, used by everyone from banks to governments
    - 256-bit key: Enough entropy to survive until the heat death of the universe
    - GCM mode: Authenticated encryption - if anyone tampers, decryption fails

    The output format (v4.0.0):
    ┌──────────────────────────────────────────────────────────────────────┐
    │ \x89ST3 │ 05 │ flags │  salt (32B)  │  iv (12B)  │  tag (16B)  │ ··· │
    │  magic  │ver │       │              │            │             │cipher│
    └──────────────────────────────────────────────────────────────────────┘

    Why the random padding at the end?
    - Message length can reveal information (traffic analysis)
    - We add 64-319 random bytes and round to 256-byte boundary
    - All messages look roughly the same size

    Args:
        message: Message string, raw bytes, or FilePayload to encrypt
        photo_data: Reference photo bytes (your "key photo")
        passphrase: Shared passphrase (recommend 4+ words from BIP-39)
        pin: Optional static PIN for additional security
        rsa_key_data: Optional RSA key bytes (another "something you have")
        channel_key: Channel key parameter:
            - None or "auto": Use server's configured key
            - str: Use this specific key
            - "" or False: No channel key (public mode)

    Returns:
        Encrypted message bytes ready for embedding

    Raises:
        EncryptionError: If encryption fails (shouldn't happen with valid inputs)
    """
    try:
        salt = secrets.token_bytes(SALT_SIZE)
        root_key = derive_hybrid_key(photo_data, passphrase, salt, pin, rsa_key_data, channel_key)

        # v6: Per-message key via HKDF — each message gets a unique encryption key
        message_nonce = secrets.token_bytes(MESSAGE_NONCE_SIZE)
        key = derive_message_key(root_key, message_nonce)

        iv = secrets.token_bytes(IV_SIZE)

        # Determine flags
        flags = 0
        channel_hash = _resolve_channel_key(channel_key)
        if channel_hash:
            flags |= FLAG_CHANNEL_KEY

        # Pack payload with type marker
        packed_payload, _ = _pack_payload(message)

        logger.debug(
            "encrypt_message: packed_payload=%d bytes, flags=0x%02x, format_version=%d",
            len(packed_payload),
            flags,
            FORMAT_VERSION,
        )

        # Random padding to hide message length
        padding_len = secrets.randbelow(256) + 64
        padded_len = ((len(packed_payload) + padding_len + 255) // 256) * 256
        padding_needed = padded_len - len(packed_payload)
        padding = secrets.token_bytes(padding_needed - 4) + struct.pack(">I", len(packed_payload))
        padded_message = packed_payload + padding

        logger.debug(
            "Padded message: %d bytes (payload + %d padding)", len(padded_message), padding_needed
        )

        # Build header for AAD (v6: includes nonce in authenticated data)
        header = MAGIC_HEADER + bytes([FORMAT_VERSION, flags])

        # Encrypt with AES-256-GCM
        cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
        encryptor = cipher.encryptor()
        encryptor.authenticate_additional_data(header + message_nonce)
        ciphertext = encryptor.update(padded_message) + encryptor.finalize()

        total_size = (
            len(header)
            + MESSAGE_NONCE_SIZE
            + len(salt)
            + len(iv)
            + len(encryptor.tag)
            + len(ciphertext)
        )
        logger.debug(
            "Encrypted output: %d bytes (header=%d, nonce=%d, salt=%d, iv=%d, tag=%d, ct=%d)",
            total_size,
            len(header),
            MESSAGE_NONCE_SIZE,
            len(salt),
            len(iv),
            len(encryptor.tag),
            len(ciphertext),
        )

        # v6: [magic|version|flags|nonce|salt|iv|tag|ciphertext]
        return header + message_nonce + salt + iv + encryptor.tag + ciphertext

    except Exception as e:
        logger.error("Encryption failed: %s", e)
        raise EncryptionError(f"Encryption failed: {e}") from e


def parse_header(encrypted_data: bytes) -> dict | None:
    """
    Parse the header from encrypted data.

    Supports both v5 (legacy) and v6 (HKDF) header formats.

    v5: [magic:4][ver:1][flags:1][salt:32][iv:12][tag:16][ciphertext]  (66+ bytes)
    v6: [magic:4][ver:1][flags:1][nonce:16][salt:32][iv:12][tag:16][ciphertext]  (82+ bytes)

    Args:
        encrypted_data: Raw encrypted bytes

    Returns:
        Dict with version, salt, iv, tag, ciphertext, flags, and optionally
        message_nonce (v6). Returns None if invalid.
    """
    # Min v5 size: 4+1+1+32+12+16 = 66 bytes
    if len(encrypted_data) < 66 or encrypted_data[:4] != MAGIC_HEADER:
        return None

    try:
        version = encrypted_data[4]

        if version == FORMAT_VERSION:
            # v6: has message nonce
            if len(encrypted_data) < 82:
                return None
            flags = encrypted_data[5]
            offset = 6
            message_nonce = encrypted_data[offset : offset + MESSAGE_NONCE_SIZE]
            offset += MESSAGE_NONCE_SIZE
            salt = encrypted_data[offset : offset + SALT_SIZE]
            offset += SALT_SIZE
            iv = encrypted_data[offset : offset + IV_SIZE]
            offset += IV_SIZE
            tag = encrypted_data[offset : offset + TAG_SIZE]
            offset += TAG_SIZE
            ciphertext = encrypted_data[offset:]

            return {
                "version": version,
                "flags": flags,
                "has_channel_key": bool(flags & FLAG_CHANNEL_KEY),
                "message_nonce": message_nonce,
                "salt": salt,
                "iv": iv,
                "tag": tag,
                "ciphertext": ciphertext,
            }

        elif version == FORMAT_VERSION_LEGACY:
            # v5: no nonce
            flags = encrypted_data[5]
            offset = 6
            salt = encrypted_data[offset : offset + SALT_SIZE]
            offset += SALT_SIZE
            iv = encrypted_data[offset : offset + IV_SIZE]
            offset += IV_SIZE
            tag = encrypted_data[offset : offset + TAG_SIZE]
            offset += TAG_SIZE
            ciphertext = encrypted_data[offset:]

            return {
                "version": version,
                "flags": flags,
                "has_channel_key": bool(flags & FLAG_CHANNEL_KEY),
                "message_nonce": None,
                "salt": salt,
                "iv": iv,
                "tag": tag,
                "ciphertext": ciphertext,
            }

        else:
            return None

    except Exception:
        return None


def decrypt_message(
    encrypted_data: bytes,
    photo_data: bytes,
    passphrase: str,
    pin: str = "",
    rsa_key_data: bytes | None = None,
    channel_key: str | bool | None = None,
) -> DecodeResult:
    """
    Decrypt message (v4.0.0 - with channel key support).

    Args:
        encrypted_data: Encrypted message bytes
        photo_data: Reference photo bytes
        passphrase: Shared passphrase
        pin: Optional static PIN
        rsa_key_data: Optional RSA key bytes
        channel_key: Channel key parameter (see encrypt_message)

    Returns:
        DecodeResult with decrypted content

    Raises:
        InvalidHeaderError: If data doesn't have valid Stegasoo header
        DecryptionError: If decryption fails (wrong credentials)
    """
    logger.debug("decrypt_message: %d bytes of encrypted data", len(encrypted_data))

    header = parse_header(encrypted_data)
    if not header:
        logger.error("Invalid or missing Stegasoo header in %d bytes", len(encrypted_data))
        raise InvalidHeaderError("Invalid or missing Stegasoo header")

    logger.debug(
        "Header: version=%d, flags=0x%02x, has_channel_key=%s, ciphertext=%d bytes",
        header["version"],
        header["flags"],
        header["has_channel_key"],
        len(header["ciphertext"]),
    )

    # Check for channel key mismatch and provide helpful error
    channel_hash = _resolve_channel_key(channel_key)
    has_configured_key = channel_hash is not None
    message_has_key = header["has_channel_key"]

    try:
        root_key = derive_hybrid_key(
            photo_data, passphrase, header["salt"], pin, rsa_key_data, channel_key
        )

        version = header["version"]
        message_nonce = header["message_nonce"]

        if version == FORMAT_VERSION and message_nonce is not None:
            # v6: Derive per-message key via HKDF
            key = derive_message_key(root_key, message_nonce)
            aad_header = MAGIC_HEADER + bytes([FORMAT_VERSION, header["flags"]]) + message_nonce
        else:
            # v5 (legacy): Root key used directly
            key = root_key
            aad_header = MAGIC_HEADER + bytes([FORMAT_VERSION_LEGACY, header["flags"]])

        cipher = Cipher(
            algorithms.AES(key), modes.GCM(header["iv"], header["tag"]), backend=default_backend()
        )
        decryptor = cipher.decryptor()
        decryptor.authenticate_additional_data(aad_header)

        padded_plaintext = decryptor.update(header["ciphertext"]) + decryptor.finalize()
        original_length = struct.unpack(">I", padded_plaintext[-4:])[0]

        logger.debug(
            "Decrypted %d bytes, original payload length: %d",
            len(padded_plaintext),
            original_length,
        )

        payload_data = padded_plaintext[:original_length]
        result = _unpack_payload(payload_data)

        logger.debug("Decryption successful: %s (v%d)", result.payload_type, version)
        return result

    except Exception as e:
        # Provide more helpful error message for channel key issues
        if message_has_key and not has_configured_key:
            raise DecryptionError(
                "Decryption failed. This message was encoded with a channel key, "
                "but no channel key is configured. Provide the correct channel key."
            ) from e
        elif not message_has_key and has_configured_key:
            raise DecryptionError(
                "Decryption failed. This message was encoded without a channel key, "
                "but you have one configured. Try with channel_key='' for public mode."
            ) from e
        else:
            # Both have or both lack channel keys — the mismatch is in
            # the key itself, the passphrase, PIN, reference photo, or RSA key.
            hint = ""
            if has_configured_key:
                from soosef.stegasoo import get_channel_fingerprint

                try:
                    current_fp = get_channel_fingerprint(
                        _resolve_channel_key(channel_key).hex()
                        if isinstance(_resolve_channel_key(channel_key), bytes)
                        else str(channel_key)
                    )
                    hint = f" Your current channel key fingerprint: {current_fp[:8]}..."
                except Exception:
                    pass
            raise DecryptionError(
                "Decryption failed. Check your passphrase, PIN, RSA key, "
                f"reference photo, and channel key.{hint}"
            ) from e


def decrypt_message_text(
    encrypted_data: bytes,
    photo_data: bytes,
    passphrase: str,
    pin: str = "",
    rsa_key_data: bytes | None = None,
    channel_key: str | bool | None = None,
) -> str:
    """
    Decrypt message and return as text string.

    For backward compatibility - returns text content or raises error for files.

    Args:
        encrypted_data: Encrypted message bytes
        photo_data: Reference photo bytes
        passphrase: Shared passphrase
        pin: Optional static PIN
        rsa_key_data: Optional RSA key bytes
        channel_key: Channel key parameter

    Returns:
        Decrypted message string

    Raises:
        DecryptionError: If decryption fails or content is a file
    """
    result = decrypt_message(encrypted_data, photo_data, passphrase, pin, rsa_key_data, channel_key)

    if result.is_file:
        if result.file_data:
            # Try to decode as text
            try:
                return result.file_data.decode("utf-8")
            except UnicodeDecodeError:
                raise DecryptionError(
                    f"Content is a binary file ({result.filename or 'unnamed'}), not text"
                )
        return ""

    return result.message or ""


def has_argon2() -> bool:
    """Check if Argon2 is available."""
    return HAS_ARGON2


# =============================================================================
# CHANNEL KEY UTILITIES (exposed for convenience)
# =============================================================================


def get_active_channel_key() -> str | None:
    """
    Get the currently configured channel key (if any).

    Returns:
        Formatted channel key string, or None if not configured
    """
    from .channel import get_channel_key

    return get_channel_key()


def get_channel_fingerprint(key: str | None = None) -> str | None:
    """
    Get a display-safe fingerprint of a channel key.

    Args:
        key: Channel key (if None, uses configured key)

    Returns:
        Masked key like "ABCD-••••-••••-••••-••••-••••-••••-3456" or None
    """
    from .channel import get_channel_fingerprint as _get_fingerprint

    return _get_fingerprint(key)
