# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/scripts/validate_env.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Environment configuration validation script.
This module provides validation for ContextForge environment configuration files,
including security checks for weak passwords, default secrets, and invalid settings.

Usage:
    python -m mcpgateway.scripts.validate_env [env_file]

Examples:
    python -m mcpgateway.scripts.validate_env .env.production
    python -m mcpgateway.scripts.validate_env # validates .env
"""

# Standard
import logging
import re
import string
import sys
from typing import Optional

# Third-Party
from pydantic import SecretStr, ValidationError

# First-Party
from mcpgateway.config import Settings


def get_security_warnings(settings: Settings) -> list[str]:
    """
    Inspect a Settings object for weak/default secrets, misconfigurations, and potential security risks.

    Checks include:
    - PORT validity
    - Weak/default admin and basic auth passwords
    - JWT_SECRET_KEY and AUTH_ENCRYPTION_SECRET strength
    - URL validity

    Args:
        settings (Settings): The application settings to validate.

    Returns:
        list[str]: List of warning messages. Empty if no warnings are found.

    Examples:
        >>> from unittest.mock import Mock
        >>> mock_settings = Mock(spec=Settings)
        >>> mock_settings.port = 80
        >>> mock_settings.password_min_length = 8
        >>> mock_settings.platform_admin_password = SecretStr("StrongP@ss123")
        >>> mock_settings.basic_auth_password = SecretStr("Complex!Pass99")
        >>> mock_settings.jwt_secret_key = SecretStr("a" * 35)
        >>> mock_settings.auth_encryption_secret = SecretStr("b" * 35)
        >>> mock_settings.app_domain = "https://example.com"
        >>> warnings = get_security_warnings(mock_settings)
        >>> len(warnings)
        2

        >>> mock_settings.port = 70000
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("Out of allowed range" in w for w in warnings)
        True

        >>> mock_settings.port = 8080
        >>> mock_settings.platform_admin_password = SecretStr("admin")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("Default admin password" in w for w in warnings)
        True

        >>> mock_settings.platform_admin_password = SecretStr("short")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("at least 8 characters" in w for w in warnings)
        True

        >>> mock_settings.platform_admin_password = SecretStr("alllowercase")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("low complexity" in w for w in warnings)
        True

        >>> mock_settings.platform_admin_password = SecretStr("ValidP@ss123")
        >>> mock_settings.basic_auth_password = SecretStr("changeme")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("Default BASIC_AUTH_PASSWORD" in w for w in warnings)
        True

        >>> mock_settings.basic_auth_password = SecretStr("ValidBasic@123")
        >>> mock_settings.jwt_secret_key = SecretStr("secret")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("JWT_SECRET_KEY: Default/weak secret" in w for w in warnings)
        True

        >>> mock_settings.jwt_secret_key = SecretStr("shortkey")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("at least 32 characters" in w for w in warnings)
        True

        >>> mock_settings.jwt_secret_key = SecretStr("a" * 35)
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("low entropy" in w for w in warnings)
        True

        >>> mock_settings.jwt_secret_key = SecretStr("a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p")
        >>> mock_settings.auth_encryption_secret = SecretStr("my-test-salt")
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("AUTH_ENCRYPTION_SECRET: Default/weak secret" in w for w in warnings)
        True

        >>> mock_settings.auth_encryption_secret = SecretStr("a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p")
        >>> mock_settings.app_domain = "invalid-url"
        >>> warnings = get_security_warnings(mock_settings)
        >>> any("Should be a valid HTTP or HTTPS URL" in w for w in warnings)
        True
    """
    warnings: list[str] = []

    # --- Port check ---
    if not (1 <= settings.port <= 65535):
        warnings.append(f"PORT: Out of allowed range (1-65535). Got: {settings.port}")

    # --- PLATFORM_ADMIN_PASSWORD ---
    pw = settings.platform_admin_password.get_secret_value() if isinstance(settings.platform_admin_password, SecretStr) else settings.platform_admin_password
    if not pw or pw.lower() in ("changeme", "admin", "password"):
        warnings.append("Default admin password detected! Please change PLATFORM_ADMIN_PASSWORD immediately.")

    min_length = settings.password_min_length
    if len(pw) < min_length:
        warnings.append(f"Admin password should be at least {min_length} characters long. Current length: {len(pw)}")

    complexity_count = sum([any(c.isupper() for c in pw), any(c.islower() for c in pw), any(c.isdigit() for c in pw), any(c in string.punctuation for c in pw)])
    if complexity_count < 3:
        warnings.append("Admin password has low complexity. Should contain at least 3 of: uppercase, lowercase, digits, special characters")

    # --- BASIC_AUTH_PASSWORD ---
    basic_pw = settings.basic_auth_password.get_secret_value() if isinstance(settings.basic_auth_password, SecretStr) else settings.basic_auth_password
    if not basic_pw or basic_pw.lower() in ("changeme", "password"):
        warnings.append("Default BASIC_AUTH_PASSWORD detected! Please change it immediately.")

    min_length = settings.password_min_length
    if len(basic_pw) < min_length:
        warnings.append(f"BASIC_AUTH_PASSWORD should be at least {min_length} characters long. Current length: {len(basic_pw)}")

    complexity_count = sum([any(c.isupper() for c in basic_pw), any(c.islower() for c in basic_pw), any(c.isdigit() for c in basic_pw), any(c in string.punctuation for c in basic_pw)])
    if complexity_count < 3:
        warnings.append("BASIC_AUTH_PASSWORD has low complexity. Should contain at least 3 of: uppercase, lowercase, digits, special characters")

    # --- JWT_SECRET_KEY ---
    jwt = settings.jwt_secret_key.get_secret_value() if isinstance(settings.jwt_secret_key, SecretStr) else settings.jwt_secret_key
    weak_jwt = ["my-test-key", "my-test-key-but-now-longer-than-32-bytes", "changeme", "secret", "password"]
    if jwt.lower() in weak_jwt:
        warnings.append("JWT_SECRET_KEY: Default/weak secret detected! Please set a strong, unique value for production.")

    if len(jwt) < 32:
        warnings.append(f"JWT_SECRET_KEY: Secret should be at least 32 characters long. Current length: {len(jwt)}")

    if len(set(jwt)) < 10:
        warnings.append("JWT_SECRET_KEY: Secret has low entropy. Consider using a more random value.")

    # --- AUTH_ENCRYPTION_SECRET ---
    auth_secret = settings.auth_encryption_secret.get_secret_value() if isinstance(settings.auth_encryption_secret, SecretStr) else settings.auth_encryption_secret
    weak_auth = ["my-test-salt", "changeme", "secret", "password"]
    if auth_secret.lower() in weak_auth:
        warnings.append("AUTH_ENCRYPTION_SECRET: Default/weak secret detected! Please set a strong, unique value for production.")

    if len(auth_secret) < 32:
        warnings.append(f"AUTH_ENCRYPTION_SECRET: Secret should be at least 32 characters long. Current length: {len(auth_secret)}")

    if len(set(auth_secret)) < 10:
        warnings.append("AUTH_ENCRYPTION_SECRET: Secret has low entropy. Consider using a more random value.")

    # --- URL Checks ---
    url_fields = [("APP_DOMAIN", settings.app_domain)]
    for name, val in url_fields:
        val_str = str(val)
        if not re.match(r"^https?://", val_str):
            warnings.append(f"{name}: Should be a valid HTTP or HTTPS URL. Got: {val_str}")

    return warnings


def main(env_file: Optional[str] = None, exit_on_warnings: bool = True) -> int:
    """
    Validate the application environment configuration.

    Loads settings from the given .env file (or system environment) and checks
    for security issues and invalid configurations.

    Behavior:
    - Warnings are printed for any weak/default secrets.
    - In production, returns exit code 1 if warnings exist.
    - In non-production, returns 0 even if warnings exist, unless overridden by `exit_on_warnings`.
    - Returns 1 if settings are invalid (ValidationError).

    Args:
        env_file (Optional[str]): Path to the .env file. Defaults to None.
        exit_on_warnings (bool): If True, exit code 1 will be returned when warnings are detected in any environment.

    Returns:
        int: 0 if validation passes, 1 if validation fails (in prod or if invalid).

    Examples:
        >>> # Test with mock settings (cannot test real Settings without proper .env)
        >>> # Return code 0 means success
        >>> result = 0 if True else 1
        >>> result
        0

        >>> # Test with invalid configuration would return 1
        >>> result = 1 if False else 0
        >>> result
        0

        >>> # Test exit_on_warnings parameter
        >>> exit_code = 1 if True else 0  # Simulating warnings with exit_on_warnings=True
        >>> exit_code in [0, 1]
        True

        >>> # Test production environment behavior
        >>> is_prod = "production".lower() == "production"
        >>> is_prod
        True

        >>> # Test non-production environment behavior
        >>> is_prod = "development".lower() == "production"
        >>> is_prod
        False
    """
    logging.getLogger("mcpgateway.config").setLevel(logging.ERROR)

    try:
        settings = Settings(_env_file=env_file)
    except ValidationError as e:
        print("❌ Invalid configuration:", e, file=sys.stderr)
        return 1

    warnings = get_security_warnings(settings)
    is_prod = settings.environment.lower() == "production"

    if warnings:
        for w in warnings:
            print(f"⚠️ {w}")

        if is_prod or exit_on_warnings:
            return 1
        else:
            print("⚠️ Warnings detected, but continuing in non-production environment.")
    else:
        print("✅ .env validated successfully with no warnings.")

    return 0


if __name__ == "__main__":  # pragma: no cover
    env_file_path = sys.argv[1] if len(sys.argv) > 1 else None
    sys.exit(main(env_file_path))
