"""
ID Wispera Secure Sharing
Zero-knowledge credential sharing primitives
"""

import base64
import json
import os
from datetime import datetime, timedelta
from typing import Optional
from urllib.parse import urlparse

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

from id_wispera.types import (
    Passport,
    SharePayload,
    ShareOptions,
    ShareScope,
)


# ============================================================================
# Constants
# ============================================================================

SHARE_FORMAT_VERSION = 1


# ============================================================================
# Crypto Utilities for Sharing
# ============================================================================

def _generate_share_key() -> str:
    """Generate a random encryption key for sharing."""
    key_bytes = os.urandom(32)
    return _bytes_to_base64url(key_bytes)


def _bytes_to_base64url(data: bytes) -> str:
    """Convert bytes to base64url string (no padding)."""
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")


def _base64url_to_bytes(s: str) -> bytes:
    """Convert base64url string to bytes."""
    # Add padding back
    padding = "=" * ((4 - len(s) % 4) % 4)
    return base64.urlsafe_b64decode(s + padding)


def _encrypt_for_sharing(data: str, key_bytes: bytes) -> tuple[str, str]:
    """
    Encrypt data for sharing using AES-GCM.
    Returns (encrypted_b64url, iv_b64url).
    """
    iv = os.urandom(12)
    aesgcm = AESGCM(key_bytes)
    encrypted = aesgcm.encrypt(iv, data.encode("utf-8"), None)
    return _bytes_to_base64url(encrypted), _bytes_to_base64url(iv)


def _decrypt_shared(encrypted_b64url: str, iv_b64url: str, key_bytes: bytes) -> str:
    """Decrypt shared data using AES-GCM."""
    iv = _base64url_to_bytes(iv_b64url)
    encrypted = _base64url_to_bytes(encrypted_b64url)
    aesgcm = AESGCM(key_bytes)
    decrypted = aesgcm.decrypt(iv, encrypted, None)
    return decrypted.decode("utf-8")


# ============================================================================
# Share Payload Encoding
# ============================================================================

def _encode_payload(payload: SharePayload) -> str:
    """Encode share payload to a shareable string."""
    json_str = payload.model_dump_json(by_alias=True)
    return _bytes_to_base64url(json_str.encode("utf-8"))


def _decode_payload(encoded: str) -> SharePayload:
    """Decode share payload from string."""
    json_bytes = _base64url_to_bytes(encoded)
    data = json.loads(json_bytes.decode("utf-8"))
    return SharePayload(**data)


# ============================================================================
# Passport Filtering by Scope
# ============================================================================

def _filter_passport_by_scope(passport: Passport, scope: ShareScope) -> dict:
    """Filter passport data based on share scope."""
    scope_val = scope.value if isinstance(scope, ShareScope) else scope

    if scope_val == "metadata-only":
        # Exclude credentialValue
        data = passport.model_dump(by_alias=True, mode="json")
        data.pop("credentialValue", None)
        return data
    else:
        # 'full' and 'read-only' both include everything
        return passport.model_dump(by_alias=True, mode="json")


# ============================================================================
# Share Operations
# ============================================================================

def create_share_link(
    passport: Passport,
    options: ShareOptions,
) -> tuple[str, str]:
    """
    Create a share link for a passport.
    Returns the encrypted payload and the decryption key separately.

    Args:
        passport: The passport to share
        options: Share options (scope, expiration, etc.)

    Returns:
        Tuple of (payload_string, key_string)

    Raises:
        ValueError: If share options are invalid
    """
    # Validate options
    if options.expires_at:
        expires_at = datetime.fromisoformat(options.expires_at.replace("Z", "+00:00"))
        if expires_at <= datetime.utcnow().replace(tzinfo=expires_at.tzinfo):
            raise ValueError("Share expiration must be in the future")

    # Filter passport data based on scope
    filtered_passport = _filter_passport_by_scope(passport, options.scope)

    # Generate encryption key
    share_key = _generate_share_key()
    key_bytes = _base64url_to_bytes(share_key)

    # Encrypt the passport data
    data_to_encrypt = json.dumps(filtered_passport, default=str)
    encrypted, iv = _encrypt_for_sharing(data_to_encrypt, key_bytes)

    # Create payload
    share_payload = SharePayload(
        version=SHARE_FORMAT_VERSION,
        encrypted=f"{iv}.{encrypted}",
        options=ShareOptions(
            scope=options.scope,
            expires_at=options.expires_at,
            max_views=options.max_views,
            require_verification=options.require_verification,
        ),
        created_at=datetime.utcnow().isoformat(),
        passport_id=passport.id,
        view_count=0,
    )

    return _encode_payload(share_payload), share_key


def resolve_share_link(payload: str, key: str) -> Passport:
    """
    Resolve (decrypt) a shared passport.

    Args:
        payload: The encoded share payload
        key: The decryption key

    Returns:
        The decrypted Passport

    Raises:
        ValueError: If payload is invalid, expired, or key is wrong
    """
    # Decode payload
    try:
        share_payload = _decode_payload(payload)
    except Exception:
        raise ValueError("Invalid share payload format")

    # Check version
    if share_payload.version != SHARE_FORMAT_VERSION:
        raise ValueError(
            f"Unsupported share format version: {share_payload.version}"
        )

    # Check expiration
    if share_payload.options.expires_at:
        expires_at = datetime.fromisoformat(
            share_payload.options.expires_at.replace("Z", "+00:00")
        )
        now = datetime.utcnow()
        if expires_at.tzinfo:
            now = now.replace(tzinfo=expires_at.tzinfo)
        if now > expires_at:
            raise ValueError("Share link has expired")

    # Check view count
    if (
        share_payload.options.max_views is not None
        and share_payload.view_count >= share_payload.options.max_views
    ):
        raise ValueError("Share link has reached maximum views")

    # Decrypt
    try:
        key_bytes = _base64url_to_bytes(key)
        parts = share_payload.encrypted.split(".")
        if len(parts) != 2:
            raise ValueError("Invalid encrypted data format")

        iv, encrypted = parts[0], parts[1]
        decrypted = _decrypt_shared(encrypted, iv, key_bytes)
        passport_data = json.loads(decrypted)
        return Passport(**passport_data)
    except ValueError:
        raise
    except Exception:
        raise ValueError(
            "Failed to decrypt share - invalid key or corrupted data"
        )


def get_share_info(payload: str) -> dict:
    """
    Get share payload info without decrypting.

    Args:
        payload: The encoded share payload

    Returns:
        Dict with share metadata (no encrypted content)

    Raises:
        ValueError: If payload format is invalid
    """
    try:
        share_payload = _decode_payload(payload)
        scope_val = share_payload.options.scope
        if isinstance(scope_val, ShareScope):
            scope_val = scope_val.value

        return {
            "version": share_payload.version,
            "options": share_payload.options.model_dump(by_alias=True),
            "createdAt": share_payload.created_at,
            "passportId": share_payload.passport_id,
            "viewCount": share_payload.view_count,
            "hasCredential": scope_val != "metadata-only",
        }
    except Exception:
        raise ValueError("Invalid share payload format")


def is_share_valid(payload: str) -> tuple[bool, Optional[str]]:
    """
    Check if a share link is still valid (not expired, not maxed out).

    Args:
        payload: The encoded share payload

    Returns:
        Tuple of (is_valid, reason_if_invalid)
    """
    try:
        share_payload = _decode_payload(payload)

        # Check expiration
        if share_payload.options.expires_at:
            expires_at = datetime.fromisoformat(
                share_payload.options.expires_at.replace("Z", "+00:00")
            )
            now = datetime.utcnow()
            if expires_at.tzinfo:
                now = now.replace(tzinfo=expires_at.tzinfo)
            if now > expires_at:
                return False, "Share link has expired"

        # Check view count
        if (
            share_payload.options.max_views is not None
            and share_payload.view_count >= share_payload.options.max_views
        ):
            return False, "Share link has reached maximum views"

        return True, None
    except Exception:
        return False, "Invalid share payload format"


# ============================================================================
# Share URL Helpers
# ============================================================================

def create_share_url(base_url: str, payload: str, key: str) -> str:
    """
    Create a full share URL (for web-based sharing).
    The key is passed as a URL fragment (not sent to server).

    Args:
        base_url: Base URL of the sharing service
        payload: The encoded share payload
        key: The decryption key

    Returns:
        Full share URL string
    """
    return f"{base_url}/share/{payload}#{key}"


def parse_share_url(url: str) -> Optional[tuple[str, str]]:
    """
    Parse a share URL back into payload and key.

    Args:
        url: The share URL to parse

    Returns:
        Tuple of (payload, key) or None if invalid
    """
    try:
        parsed = urlparse(url)
        path_parts = parsed.path.split("/")

        try:
            share_index = path_parts.index("share")
        except ValueError:
            return None

        if share_index >= len(path_parts) - 1:
            return None

        payload = path_parts[share_index + 1]
        key = parsed.fragment  # After the #

        if not payload or not key:
            return None

        return payload, key
    except Exception:
        return None


# ============================================================================
# One-Time Sharing
# ============================================================================

def create_one_time_share(
    passport: Passport,
    expires_in_minutes: int = 60,
) -> tuple[str, str]:
    """
    Create a one-time share (single use, auto-expires).

    Args:
        passport: The passport to share
        expires_in_minutes: Minutes until expiration (default 60)

    Returns:
        Tuple of (payload, key)
    """
    expires_at = (
        datetime.utcnow() + timedelta(minutes=expires_in_minutes)
    ).isoformat()

    options = ShareOptions(
        scope=ShareScope.FULL,
        expires_at=expires_at,
        max_views=1,
    )

    return create_share_link(passport, options)


def create_metadata_share(
    passport: Passport,
    *,
    expires_at: Optional[str] = None,
    max_views: Optional[int] = None,
) -> tuple[str, str]:
    """
    Create a metadata-only share (no credential exposed).

    Args:
        passport: The passport to share
        expires_at: Optional expiration (ISO 8601)
        max_views: Optional maximum views

    Returns:
        Tuple of (payload, key)
    """
    options = ShareOptions(
        scope=ShareScope.METADATA_ONLY,
        expires_at=expires_at,
        max_views=max_views,
    )

    return create_share_link(passport, options)
