"""YubiKey OTP validation - zero external dependencies.

Uses Yubico's public validation API to verify YubiKey OTP tokens.
"""
import hashlib
import json
import os
import secrets
import sys
import urllib.error
import urllib.request

# Yubico validation endpoint
YUBICO_API_URL = "https://api.yubico.com/wsapi/2.0/verify"
# Client ID 1 is for testing/development
YUBICO_CLIENT_ID = "1"

CONFIG_PATH = os.path.expanduser("~/.obol/config.json")


def _generate_nonce() -> str:
    """Generate a random nonce for the verification request."""
    return secrets.token_hex(16)


def extract_public_id(otp: str) -> str:
    """Extract the public ID (first 12 chars) from a YubiKey OTP.

    The public ID identifies the specific YubiKey hardware.
    The remaining characters are the encrypted one-time password.
    """
    if len(otp) < 32:
        return ""
    return otp[:12].lower()


def validate_otp(otp: str) -> tuple[bool, str]:
    """Validate a YubiKey OTP against Yubico's API.

    Returns:
        (is_valid, message) tuple
    """
    # Basic format check
    if not otp or len(otp) < 32:
        return False, "OTP too short (minimum 32 characters)"

    # OTP should be modhex characters only
    modhex_chars = set("cbdefghijklnrtuv")
    if not all(c.lower() in modhex_chars for c in otp):
        return False, "Invalid OTP format (not modhex)"

    nonce = _generate_nonce()

    try:
        # Build verification URL
        params = f"id={YUBICO_CLIENT_ID}&otp={otp}&nonce={nonce}"
        url = f"{YUBICO_API_URL}?{params}"

        req = urllib.request.Request(
            url,
            headers={"User-Agent": "obol-sdk/0.1.0"},
        )

        with urllib.request.urlopen(req, timeout=10) as resp:
            response_text = resp.read().decode()

        # Parse response (key=value format)
        result = {}
        for line in response_text.strip().split("\n"):
            line = line.strip()
            if "=" in line:
                key, value = line.split("=", 1)
                result[key.strip()] = value.strip()

        status = result.get("status", "")

        # Verify nonce matches
        if result.get("nonce") != nonce:
            return False, "Nonce mismatch - possible replay attack"

        if status == "OK":
            return True, "YubiKey validated successfully"
        elif status == "REPLAYED_OTP":
            return False, "OTP already used - touch the key again"
        elif status == "BAD_OTP":
            return False, "Invalid OTP - make sure key is inserted correctly"
        elif status == "NO_SUCH_CLIENT":
            return False, "Yubico client ID error"
        else:
            return False, f"Validation failed: {status}"

    except urllib.error.URLError as e:
        return False, f"Network error: {e.reason}"
    except Exception as e:
        return False, f"Validation error: {e}"


def load_allowed_yubikeys() -> list[str]:
    """Load allowed YubiKey public IDs from config."""
    if not os.path.exists(CONFIG_PATH):
        return []

    try:
        with open(CONFIG_PATH) as f:
            config = json.load(f)
        return config.get("yubikey_ids", [])
    except Exception:
        return []


def save_yubikey_id(public_id: str) -> bool:
    """Add a YubiKey public ID to the allowed list."""
    public_id = public_id.lower().strip()

    if len(public_id) != 12:
        return False

    # Load existing config
    config = {}
    if os.path.exists(CONFIG_PATH):
        try:
            with open(CONFIG_PATH) as f:
                config = json.load(f)
        except Exception:
            pass

    # Get or create yubikey_ids list
    yubikey_ids = config.get("yubikey_ids", [])

    # Add if not already present
    if public_id not in yubikey_ids:
        yubikey_ids.append(public_id)
        config["yubikey_ids"] = yubikey_ids

        # Save config
        os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
        with open(CONFIG_PATH, "w") as f:
            json.dump(config, f, indent=2)

        # Secure the file
        try:
            if sys.platform != "win32":
                os.chmod(CONFIG_PATH, 0o600)
        except Exception:
            pass

    return True


def remove_yubikey_id(public_id: str) -> bool:
    """Remove a YubiKey public ID from the allowed list."""
    public_id = public_id.lower().strip()

    if not os.path.exists(CONFIG_PATH):
        return False

    try:
        with open(CONFIG_PATH) as f:
            config = json.load(f)
    except Exception:
        return False

    yubikey_ids = config.get("yubikey_ids", [])

    if public_id in yubikey_ids:
        yubikey_ids.remove(public_id)
        config["yubikey_ids"] = yubikey_ids

        with open(CONFIG_PATH, "w") as f:
            json.dump(config, f, indent=2)
        return True

    return False


def require_yubikey() -> bool:
    """Check if YubiKey authentication is required.

    Returns True if yubikey_ids is configured and non-empty.
    """
    allowed = load_allowed_yubikeys()
    return len(allowed) > 0


def prompt_and_verify_yubikey() -> bool:
    """Prompt user for YubiKey OTP and verify it.

    Returns True if verification succeeds, False otherwise.
    Exits if user cancels (Ctrl+C).
    """
    allowed_ids = load_allowed_yubikeys()

    if not allowed_ids:
        # No YubiKeys configured, skip authentication
        return True

    print("YubiKey authentication required.")
    print(f"Allowed keys: {', '.join(allowed_ids)}")
    print()

    try:
        otp = input("Touch your YubiKey: ").strip()
    except (KeyboardInterrupt, EOFError):
        print("\nAuthentication cancelled.")
        return False

    if not otp:
        print("No OTP provided.")
        return False

    # Extract public ID and check if it's allowed
    public_id = extract_public_id(otp)
    if not public_id:
        print("Invalid OTP format.")
        return False

    if public_id not in allowed_ids:
        print(f"YubiKey {public_id} is not authorized.")
        return False

    # Validate with Yubico
    is_valid, message = validate_otp(otp)

    if is_valid:
        print(f"Authenticated with YubiKey {public_id}")
        return True
    else:
        print(f"Authentication failed: {message}")
        return False
