###
# Copyright 2025 Hewlett Packard Enterprise, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###

# -*- coding: utf-8 -*-
"""Centralized security masking framework for iLORest"""

import json
import re
import copy
from typing import Dict, List, Any, Union, Optional


class SecurityMasker:
    """Centralized framework for masking sensitive information in logs and outputs."""

    # Default mask character
    MASK_VALUE = "******"

    # Number of characters to show for partial masking
    PARTIAL_MASK_SHOW_CHARS = 4

    # Sensitive command-line argument patterns
    SENSITIVE_CLI_KEYWORDS = {
        "--password",
        "--pass",
        "-p",
        "--pwd",
        "--token",
        "--auth-token",
        "--session-token",
        "--key",
        "--api-key",
        "--secret",
        "--credential",
        "--cert-key",
        "--private-key",
        "--passphrase",
        "--sessionkey",
        "--auth",
        "--authorization",
        "--filepass",
        "--backup-password",
        "--biospassword",
    }

    # Fields requiring COMPLETE masking (no characters visible)
    COMPLETE_MASK_FIELDS = {
        "password",
        "oldpassword",
        "newpassword",
        "currentpassword",
        "passwd",
        "pwd",
        "pass",
        "passphrase",
        "secret",
        "client_secret",
        "private_key",
        "privatekey",
        "cert_key",
        "certkey",
        "pin",
        "otp",
        "totp",
        "mfa_token",
        "csrf_token",
        "csrftoken",
    }

    # Fields allowing PARTIAL masking (last 4-6 chars visible for debugging)
    PARTIAL_MASK_FIELDS = {
        "token",
        "auth_token",
        "authtoken",
        "session_token",
        "sessiontoken",
        "access_token",
        "accesstoken",
        "refresh_token",
        "refreshtoken",
        "bearer_token",
        "api_key",
        "apikey",
        "sessionkey",
        "session_key",
        "credential",
        "credentials",
        "appid",
        "app_id",
        "applicationid",
        "application_id",
    }

    # Headers requiring COMPLETE masking
    COMPLETE_MASK_HEADERS = {
        "authorization",  # Often contains Basic auth with password
        "www-authenticate",
        "proxy-authorization",
        "cookie",
        "set-cookie",  # May contain sensitive session data
    }

    # Headers allowing PARTIAL masking (for debugging/correlation)
    PARTIAL_MASK_HEADERS = {
        "x-auth-token",
        "x-session-token",
        "x-api-key",
        "api-key",
        "bearer",  # When separate from Authorization header
        "x-session-id",
        "session-id",
    }

    # All sensitive headers (union of complete and partial)
    SENSITIVE_HEADERS = (
        COMPLETE_MASK_HEADERS
        | PARTIAL_MASK_HEADERS
        | {"authentication", "basic", "digest", "oauth", "jwt", "x-csrf-token", "x-request-id"}
    )

    # All sensitive body fields (union of complete and partial)
    SENSITIVE_BODY_FIELDS = COMPLETE_MASK_FIELDS | PARTIAL_MASK_FIELDS | {"auth", "authorization"}

    # URL parameter patterns that might contain sensitive data
    SENSITIVE_URL_PARAMS = {"password", "token", "key", "secret", "auth", "session", "credential", "pass", "pwd"}

    @classmethod
    def mask_command_arguments(cls, args: List[str]) -> List[str]:
        """
        Mask sensitive information in command-line arguments.

        Args:
            args: List of command-line arguments

        Returns:
            List of arguments with sensitive values masked

        Example:
            >>> SecurityMasker.mask_command_arguments(['--user', 'admin', '--password', 'MyP@ss'])
            ['--user', 'admin', '--password', '******']
        """
        if not args:
            return args

        masked_args = []
        i = 0

        while i < len(args):
            arg = args[i]

            # Check for inline sensitive arguments (--password=value)
            if cls._has_inline_sensitive_value(arg):
                masked_args.append(cls._mask_inline_argument(arg))
                i += 1
                continue

            # Check if current arg is a sensitive flag
            if cls._is_sensitive_cli_flag(arg):
                masked_args.append(arg)  # Keep the flag
                # Mask the next argument if it exists and isn't another flag
                if i + 1 < len(args) and not args[i + 1].startswith("-"):
                    i += 1  # Move to the next argument
                    masked_args.append(cls.MASK_VALUE)  # Mask the value
                i += 1
                continue

            # Keep non-sensitive arguments as-is
            masked_args.append(arg)
            i += 1

        return masked_args

    @classmethod
    def mask_http_headers(
        cls, headers: Union[Dict[str, str], List[tuple], Any]
    ) -> Union[Dict[str, str], List[tuple], str]:
        """
        Mask sensitive information in HTTP headers.

        Args:
            headers: HTTP headers in various formats (dict, list of tuples, or other)

        Returns:
            Headers with sensitive values masked

        Examples:
            # dict format
            >>> SecurityMasker.mask_http_headers({'Authorization': 'Basic abc123', 'Accept': 'application/json'})
            {'Authorization': '******', 'Accept': 'application/json'}

            # list-of-tuples format
            >>> SecurityMasker.mask_http_headers([('Authorization', 'Bearer abcd'), ('Host', 'example')])
            [('Authorization', '******'), ('Host', 'example')]
        """
        if not headers:
            return headers

        try:
            # Handle dictionary format
            if isinstance(headers, dict):
                return cls._mask_headers_dict(headers)

            # Handle list of tuples format
            elif isinstance(headers, (list, tuple)):
                return cls._mask_headers_list(headers)

            # Handle string representation of headers
            elif isinstance(headers, str):
                return cls._mask_headers_string(headers)

            # Handle other object types (e.g., HTTPMessage)
            else:
                return cls._mask_headers_object(headers)

        except Exception:
            # If masking fails, return a safe placeholder
            return "[HEADERS MASKED DUE TO PROCESSING ERROR]"

    @classmethod
    def mask_simple_body(cls, body: Union[str, bytes, Dict[str, Any]]) -> Union[str, bytes, Dict[str, Any]]:
        """
        Mask sensitive information in simple HTTP request/response bodies.

        This method handles basic body masking for simple payloads.
        For complex bodies with advanced JSON parsing and error handling,
        use mask_complex_body() instead.

        Args:
            body: Request/response body in various formats

        Returns:
            Body with sensitive values masked

        Examples:
            # JSON string
            >>> SecurityMasker.mask_simple_body('{"password":"secret"}')
            '{"password":"******"}'

            # dict
            >>> SecurityMasker.mask_simple_body({"token": "abcd"})
            {"token": "******"}
        """
        if not body:
            return body

        try:
            # Handle string body (likely JSON)
            if isinstance(body, str):
                return cls._mask_string_body(body)

            # Handle bytes body
            elif isinstance(body, bytes):
                return cls._mask_bytes_body(body)

            # Handle dictionary body
            elif isinstance(body, dict):
                return cls._mask_dict_body(body)

            # Handle other types
            else:
                return body

        except Exception:
            # If masking fails, return original or safe placeholder
            if isinstance(body, (str, bytes)):
                return "[BODY MASKED DUE TO PROCESSING ERROR]"
            return body

    @classmethod
    def mask_url_parameters(cls, url: str) -> str:
        """
        Mask sensitive information in URL parameters.

        Args:
            url: URL string that may contain parameters

        Returns:
            URL with sensitive parameter values masked

        Example:
            >>> SecurityMasker.mask_url_parameters('https://api.example.com/login?user=admin&password=1234')
            'https://api.example.com/login?user=admin&password=******'
        """
        if not url or "?" not in url:
            return url

        try:
            base_url, params = url.split("?", 1)
            param_pairs = params.split("&")
            masked_pairs = []

            for pair in param_pairs:
                if "=" in pair:
                    key, value = pair.split("=", 1)
                    if any(sensitive in key.lower() for sensitive in cls.SENSITIVE_URL_PARAMS):
                        masked_pairs.append(f"{key}={cls.MASK_VALUE}")
                    else:
                        masked_pairs.append(pair)
                else:
                    masked_pairs.append(pair)

            return f"{base_url}?{'&'.join(masked_pairs)}"

        except Exception:
            return url

    @classmethod
    def mask_complex_body(cls, body: Union[str, bytes, Dict[str, Any]]) -> Union[str, bytes]:
        """
        Mask sensitive information in complex HTTP request/response bodies for logging.

        This is a specialized method for complex body masking that handles:
        - Advanced JSON parsing with error recovery
        - Automatic encoding/decoding (str ↔ bytes)
        - Type preservation (returns same type as input)
        - Graceful error handling to ensure logging never fails

        Use this method for complex request/response bodies. For simple bodies,
        mask_simple_body() may be sufficient.

        Args:
            body: The request/response body in various formats (str, bytes, dict)

        Returns:
            Masked body as string or bytes (matching input type)

        Example:
            >>> SecurityMasker.mask_complex_body(b'{"users":[{"name":"u","password":"p"}]}')
            b'{"users": [{"name": "u", "password": "******"}]}'
        """
        if not body:
            return body

        original_type = type(body)

        try:
            # Convert to string if bytes
            if isinstance(body, bytes):
                try:
                    body = body.decode("utf-8")
                except UnicodeDecodeError:
                    # If decode fails, return original bytes
                    return body

            # If already a dict, mask and return as JSON string
            if isinstance(body, dict):
                masked_dict = cls._mask_dict_body(body)
                return json.dumps(masked_dict)

            # If string, check if it's JSON
            if isinstance(body, str):
                body = body.strip()
                if body.startswith(("{", "[")):
                    try:
                        # Parse JSON and mask
                        body_json = json.loads(body)
                        masked_json = cls._mask_dict_body(body_json)
                        masked_string = json.dumps(masked_json)

                        # Return in original type
                        if original_type == bytes:
                            return masked_string.encode("utf-8")
                        return masked_string
                    except json.JSONDecodeError:
                        # Not valid JSON, return as-is
                        pass

            # Return body as-is if not JSON or other type
            if original_type == bytes and isinstance(body, str):
                return body.encode("utf-8")
            return body

        except Exception:
            # If anything fails, return original to ensure logging continues
            # Converting to string if needed for logging
            if isinstance(body, bytes):
                try:
                    return body.decode("utf-8", errors="replace")
                except:
                    return "[BODY - BINARY DATA]"
            return body if body else ""

    @classmethod
    def mask_user_input(cls, user_input: str, command_context: Optional[str] = None) -> str:
        """
        Mask sensitive information in user input based on context.

        Args:
            user_input: Raw user input string
            command_context: Optional context about what command is being executed

        Returns:
            User input with sensitive parts masked

        Example:
            >>> SecurityMasker.mask_user_input('--login admin --password MyP@ss')
            '--login admin --password ******'
        """
        if not user_input:
            return user_input

        # Split input into tokens for analysis
        tokens = user_input.split()

        # Apply command argument masking
        masked_tokens = cls.mask_command_arguments(tokens)

        return " ".join(masked_tokens)

    # Private helper methods
    @classmethod
    def _is_sensitive_cli_flag(cls, arg: str) -> bool:
        """Check if an argument is a sensitive command-line flag."""
        return arg.lower() in {flag.lower() for flag in cls.SENSITIVE_CLI_KEYWORDS}

    @classmethod
    def _has_inline_sensitive_value(cls, arg: str) -> bool:
        """Check if an argument contains inline sensitive value (flag=value)."""
        if "=" not in arg:
            return False
        flag = arg.split("=")[0]
        return cls._is_sensitive_cli_flag(flag)

    @classmethod
    def _mask_inline_argument(cls, arg: str) -> str:
        """Mask the value part of an inline argument."""
        return re.sub(r"=(.*)", f"={cls.MASK_VALUE}", arg)

    @classmethod
    def _mask_headers_dict(cls, headers: Dict[str, str]) -> Dict[str, str]:
        """Mask sensitive headers in dictionary format."""
        masked = {}
        for key, value in headers.items():
            if cls._is_sensitive_header(key):
                # Determine if complete or partial masking
                masked[key] = cls._mask_value(value, key, is_header=True)
            else:
                masked[key] = value
        return masked

    @classmethod
    def _mask_headers_list(cls, headers: List[tuple]) -> List[tuple]:
        """Mask sensitive headers in list of tuples format."""
        masked = []
        for item in headers:
            if isinstance(item, (list, tuple)) and len(item) >= 2:
                key, value = item[0], item[1]
                if cls._is_sensitive_header(key):
                    masked_value = cls._mask_value(value, key, is_header=True)
                    masked.append((key, masked_value))
                else:
                    masked.append(item)
            else:
                masked.append(item)
        return masked

    @classmethod
    def _mask_headers_string(cls, headers: str) -> str:
        """Mask sensitive headers in string format."""
        lines = headers.split("\n")
        masked_lines = []

        for line in lines:
            if ":" in line:
                key, value = line.split(":", 1)
                if cls._is_sensitive_header(key.strip()):
                    # Preserve leading whitespace (indentation) from original key
                    leading_space = key[: len(key) - len(key.lstrip())]
                    masked_value = cls._mask_value(value.strip(), key.strip(), is_header=True)
                    masked_lines.append(f"{leading_space}{key.strip()}: {masked_value}")
                else:
                    masked_lines.append(line)
            else:
                masked_lines.append(line)

        return "\n".join(masked_lines)

    @classmethod
    def _mask_headers_object(cls, headers: Any) -> str:
        """Mask sensitive headers in object format (e.g., HTTPMessage)."""
        try:
            # Try to convert to dictionary first
            if hasattr(headers, "items"):
                header_dict = dict(headers.items())
                masked_dict = cls._mask_headers_dict(header_dict)
                return str(masked_dict)
            elif hasattr(headers, "__dict__"):
                return cls._mask_headers_dict(headers.__dict__)
            else:
                return "[HEADERS MASKED - UNKNOWN FORMAT]"
        except Exception:
            return "[HEADERS MASKED DUE TO PROCESSING ERROR]"

    @classmethod
    def _is_sensitive_header(cls, header_name: str) -> bool:
        """Check if a header name is considered sensitive."""
        header_lower = header_name.lower().strip()
        return any(sensitive in header_lower for sensitive in cls.SENSITIVE_HEADERS)

    @classmethod
    def _mask_string_body(cls, body: str) -> str:
        """Mask sensitive information in string body."""
        try:
            # Try to parse as JSON
            if body.strip().startswith(("{", "[")):
                json_body = json.loads(body)
                masked_body = cls._mask_dict_body(json_body)
                return json.dumps(masked_body)
            else:
                # Handle form data or other string formats
                return cls._mask_form_data(body)
        except json.JSONDecodeError:
            # Not JSON, treat as form data or plain text
            return cls._mask_form_data(body)

    @classmethod
    def _mask_bytes_body(cls, body: bytes) -> bytes:
        """Mask sensitive information in bytes body."""
        try:
            # Convert to string, mask, then back to bytes
            string_body = body.decode("utf-8")
            masked_string = cls._mask_string_body(string_body)
            return masked_string.encode("utf-8")
        except UnicodeDecodeError:
            # If can't decode, return as-is (likely binary data)
            return body

    @classmethod
    def _mask_dict_body(cls, body: Dict[str, Any]) -> Dict[str, Any]:
        """Mask sensitive information in dictionary body."""
        masked = copy.deepcopy(body)

        def mask_recursive(obj):
            if isinstance(obj, dict):
                for key, value in obj.items():
                    if cls._is_sensitive_body_field(key):
                        # Apply appropriate masking based on field type
                        obj[key] = cls._mask_value(str(value) if value else "", key, is_header=False)
                    else:
                        mask_recursive(value)
            elif isinstance(obj, list):
                for item in obj:
                    mask_recursive(item)

        mask_recursive(masked)
        return masked

    @classmethod
    def _mask_form_data(cls, data: str) -> str:
        """Mask sensitive information in form data."""
        # Handle URL-encoded form data
        if "&" in data and "=" in data:
            pairs = data.split("&")
            masked_pairs = []
            for pair in pairs:
                if "=" in pair:
                    key, value = pair.split("=", 1)
                    if cls._is_sensitive_body_field(key):
                        masked_pairs.append(f"{key}={cls.MASK_VALUE}")
                    else:
                        masked_pairs.append(pair)
                else:
                    masked_pairs.append(pair)
            return "&".join(masked_pairs)

        # For other string formats, return as-is
        return data

    @classmethod
    def _is_sensitive_body_field(cls, field_name: str) -> bool:
        """Check if a body field name is considered sensitive."""
        field_lower = field_name.lower().strip()
        return field_lower in cls.SENSITIVE_BODY_FIELDS

    @classmethod
    def _mask_value(cls, value: str, field_name: str, is_header: bool = False) -> str:
        """
        Mask a sensitive value with either complete or partial masking.

        Args:
            value: The value to mask
            field_name: The field/header name (for determining mask type)
            is_header: Whether this is a header (vs body field)

        Returns:
            Masked value (either completely or partially)
        """
        if not value:
            return cls.MASK_VALUE

        field_lower = field_name.lower().strip()

        # Determine if we should use complete or partial masking
        use_complete_mask = False

        if is_header:
            # Check if header requires complete masking
            use_complete_mask = field_lower in {h.lower() for h in cls.COMPLETE_MASK_HEADERS}
            # Special case: Authorization header with "Basic" is password-based
            if field_lower == "authorization" and value.strip().lower().startswith("basic"):
                use_complete_mask = True
        else:
            # Check if body field requires complete masking
            use_complete_mask = field_lower in {f.lower() for f in cls.COMPLETE_MASK_FIELDS}

        # Apply appropriate masking
        if use_complete_mask:
            return cls.MASK_VALUE
        else:
            # Partial masking: show last N characters
            return cls._partial_mask(value)

    @classmethod
    def _partial_mask(cls, value: str, show_chars: int = None) -> str:
        """
        Apply partial masking to a value, showing only the last N characters.

        Args:
            value: The value to mask
            show_chars: Number of characters to show (default: PARTIAL_MASK_SHOW_CHARS)

        Returns:
            Partially masked value like "******a0fb"
        """
        if show_chars is None:
            show_chars = cls.PARTIAL_MASK_SHOW_CHARS

        if not value or len(value) <= show_chars:
            # If value is too short, completely mask it
            return cls.MASK_VALUE

        # Show last N characters
        visible_part = value[-show_chars:]
        return f"{cls.MASK_VALUE}{visible_part}"


# Convenience functions for backward compatibility and easy usage
def mask_passwords(args: List[str]) -> List[str]:
    """Backward compatible function for masking passwords in command arguments."""
    return SecurityMasker.mask_command_arguments(args)


def mask_http_request_data(method: str, path: str, headers: Any, body: Any) -> tuple:
    """
    Mask all sensitive data in an HTTP request for logging.

    Args:
        method: HTTP method
        path: Request path/URL
        headers: Request headers
        body: Request body

    Returns:
        Tuple of (method, masked_path, masked_headers, masked_body)
    """
    masked_path = SecurityMasker.mask_url_parameters(path)
    masked_headers = SecurityMasker.mask_http_headers(headers)
    masked_body = SecurityMasker.mask_simple_body(body)

    return method, masked_path, masked_headers, masked_body


def mask_user_input(user_input: str, command_context: Optional[str] = None) -> str:
    """Convenience function for masking user input."""
    return SecurityMasker.mask_user_input(user_input, command_context)
