"""
Pandas transform functions for Component Vulnerability Analysis (CVA) report.
"""

from typing import Any

import numpy as np
import pandas as pd

from fs_report.models import Config


def component_vulnerability_analysis_pandas_transform(
    data: list[dict[str, Any]] | pd.DataFrame, config: Config
) -> pd.DataFrame:
    """
    Transform findings data for Component Vulnerability Analysis with portfolio risk scoring.

    Args:
        data: Raw findings data from API (list of dicts or DataFrame)
        config: Configuration object

    Returns:
        Processed DataFrame with component risk analysis
    """
    import logging

    logger = logging.getLogger(__name__)

    if isinstance(data, pd.DataFrame):
        logger.debug(f"CVA transform called with {len(data)} records (DataFrame)")
        if data.empty:
            logger.warning("CVA transform: No data provided")
            return pd.DataFrame()
        df = data.copy()
    else:
        logger.debug(f"CVA transform called with {len(data) if data else 0} records")
        if not data:
            logger.warning("CVA transform: No data provided")
            return pd.DataFrame()
        # Convert to DataFrame
        df = pd.DataFrame(data)
    logger.debug(f"CVA transform: DataFrame created with shape {df.shape}")
    logger.debug(f"CVA transform: DataFrame columns: {list(df.columns)}")

    # Note: Project filtering is already applied at the API level, so we don't need to filter again here

    # Handle both flattened and unflattened data
    if "component.name" in df.columns:
        # Data is already flattened
        logger.debug("CVA transform: Using flattened data (component.name exists)")
        df["name"] = df["component.name"]
        df["version"] = df["component.version"]
        df["project_name"] = df["project.name"]
    elif "component" in df.columns:
        # Data is not flattened, extract from nested structure
        logger.debug("CVA transform: Using nested data (component exists)")
        df["name"] = df["component"].apply(
            lambda x: x.get("name", "Unknown") if isinstance(x, dict) else "Unknown"
        )
        df["version"] = df["component"].apply(
            lambda x: x.get("version", "Unknown") if isinstance(x, dict) else "Unknown"
        )
        df["project_name"] = df["project"].apply(
            lambda x: x.get("name", "Unknown") if isinstance(x, dict) else "Unknown"
        )
    else:
        # Fallback - try to extract from any available columns
        logger.warning("CVA transform: No component data found, using fallback")
        df["name"] = "Unknown"
        df["version"] = "Unknown"
        df["project_name"] = "Unknown"

    # Handle exploit info for has_exploits calculation
    if "exploitInfo" in df.columns:
        df["hasKnownExploit"] = df["exploitInfo"].apply(
            lambda x: isinstance(x, list) and len(x) > 0
        )
    else:
        df["hasKnownExploit"] = False

    # Ensure required columns exist with defaults
    required_defaults = {
        "name": "Unknown",
        "version": "Unknown",
        "project_name": "Unknown",
        "inKev": False,
        "inVcKev": False,
        "epssPercentile": 0.0,
        "reachabilityScore": 0.0,
        "severity": "LOW",
        "risk": 0,
    }

    for col, default_val in required_defaults.items():
        if col not in df.columns:
            df[col] = default_val  # type: ignore[call-overload]

    # Apply component filter if specified
    component_filter = getattr(config, "component_filter", None)
    if component_filter:
        from fs_report.transforms.pandas._component_filter import (
            apply_component_filter,
        )

        match_mode = getattr(config, "component_match", "contains")
        df = apply_component_filter(
            df,
            component_filter,
            match_mode=match_mode,
            name_col="name",
            version_col="version",
        )
        if df.empty:
            logger.warning("Component filter returned 0 findings")
            return pd.DataFrame()

    # Group by component to calculate portfolio metrics
    grouped_df = calculate_portfolio_risk_scores(df)

    # Sort by composite risk score (descending)
    grouped_df = grouped_df.sort_values("portfolio_composite_risk", ascending=False)

    # Calculate cumulative percentage (running sum / total)
    total_risk = grouped_df["portfolio_composite_risk"].sum()
    grouped_df["cumulative_percentage"] = (
        (grouped_df["portfolio_composite_risk"].cumsum() / total_risk * 100)
        if total_risk > 0
        else 0.0
    )

    # Convert risk scores to integers
    grouped_df["portfolio_composite_risk"] = grouped_df[
        "portfolio_composite_risk"
    ].astype(int)
    grouped_df["normalized_risk_score"] = grouped_df["normalized_risk_score"].astype(
        int
    )

    # Select final columns
    final_columns = [
        "name",
        "version",
        "portfolio_composite_risk",
        "normalized_risk_score",
        "cumulative_percentage",
        "findings_count",
        "project_count",
        "has_kev",
        "has_exploits",
        "project_names",
    ]

    result_df = grouped_df[final_columns].copy()

    logger.debug(f"CVA transform: Final result DataFrame shape: {result_df.shape}")
    logger.debug(f"CVA transform: Final result columns: {list(result_df.columns)}")
    if not result_df.empty:
        logger.debug(f"CVA transform: Sample data: {result_df.head(2).to_dict()}")
    else:
        logger.warning("CVA transform: Final result DataFrame is empty!")

    return result_df


def flatten_cva_data(df: pd.DataFrame) -> pd.DataFrame:
    """
    Flatten nested data structures in CVA DataFrame.

    Args:
        df: Raw findings DataFrame

    Returns:
        Flattened DataFrame with component and project data extracted
    """
    import ast

    # Handle component data
    if "component" in df.columns:

        def extract_component_name(component: Any) -> str:
            if isinstance(component, dict):
                return str(component.get("name", "Unknown"))
            if isinstance(component, str):
                try:
                    comp = ast.literal_eval(component)
                    if isinstance(comp, dict):
                        return str(comp.get("name", "Unknown"))
                except Exception:
                    pass
                return component.strip() if component.strip() else "Unknown"
            return "Unknown"

        def extract_component_version(component: Any) -> str:
            if isinstance(component, dict):
                return str(component.get("version", "Unknown"))
            if isinstance(component, str):
                try:
                    comp = ast.literal_eval(component)
                    if isinstance(comp, dict):
                        return str(comp.get("version", "Unknown"))
                except Exception:
                    pass
            return "Unknown"

        df["name"] = df["component"].apply(extract_component_name)
        df["version"] = df["component"].apply(extract_component_version)

    # Handle project data
    if "project" in df.columns:

        def extract_project_name(project: Any) -> str:
            if isinstance(project, dict):
                return str(project.get("name", "Unknown"))
            if isinstance(project, str):
                try:
                    proj = ast.literal_eval(project)
                    if isinstance(proj, dict):
                        return str(proj.get("name", "Unknown"))
                except Exception:
                    pass
                return project.strip() if project.strip() else "Unknown"
            return "Unknown"

        df["project_name"] = df["project"].apply(extract_project_name)

    # Handle exploit info for has_exploits calculation
    if "exploitInfo" in df.columns:

        def has_exploits(exploit_info: Any) -> bool:
            if isinstance(exploit_info, list) and len(exploit_info) > 0:
                return True
            return False

        df["hasKnownExploit"] = df["exploitInfo"].apply(has_exploits)
    else:
        df["hasKnownExploit"] = False

    # Ensure required columns exist
    if "inKev" not in df.columns:
        df["inKev"] = False
    if "inVcKev" not in df.columns:
        df["inVcKev"] = False
    if "epssPercentile" not in df.columns:
        df["epssPercentile"] = 0.0
    if "reachabilityScore" not in df.columns:
        df["reachabilityScore"] = 0.0
    if "severity" not in df.columns:
        df["severity"] = "LOW"
    if "risk" not in df.columns:
        df["risk"] = 0

    return df


def calculate_portfolio_risk_scores(df: pd.DataFrame) -> pd.DataFrame:
    """
    Calculate portfolio-level risk scores by grouping components.

    Args:
        df: Flattened findings DataFrame

    Returns:
        DataFrame with aggregated component risk scores
    """

    # Group by component name and version
    def _unique_list(x: pd.Series) -> list[Any]:
        return list(x.unique())

    grouped = (
        df.groupby(["name", "version"])
        .agg(
            {
                "project_name": ["nunique", _unique_list],
                "id": "count",
                "inKev": "any",
                "hasKnownExploit": "any",
                "risk": "sum",
            }
        )
        .reset_index()
    )

    # Flatten column names
    grouped.columns = [
        "name",
        "version",
        "project_count",
        "project_names",
        "findings_count",
        "has_kev",
        "has_exploits",
        "base_risk_score",
    ]

    # Calculate severity-weighted risk scores
    def calculate_severity_risks(group_df: pd.DataFrame) -> pd.Series:
        critical_risk = group_df[group_df["severity"] == "CRITICAL"]["risk"].sum()
        high_risk = group_df[group_df["severity"] == "HIGH"]["risk"].sum()
        medium_risk = group_df[group_df["severity"] == "MEDIUM"]["risk"].sum()
        low_risk = group_df[group_df["severity"] == "LOW"]["risk"].sum()

        severity_weighted_score = (
            critical_risk * 5 + high_risk * 3 + medium_risk * 2 + low_risk * 1
        )

        # KEV and exploit bonuses
        kev_exploit_bonus = 0
        if group_df["inKev"].any() or group_df["inVcKev"].any():
            kev_exploit_bonus += 30
        if group_df["hasKnownExploit"].any():
            kev_exploit_bonus += 40

        # EPSS bonus (high percentile findings)
        epss_bonus = (group_df["epssPercentile"] > 0.95).sum() * 10

        # Reachability multiplier
        reachability_multiplier = (
            group_df[group_df["reachabilityScore"] > 0]["risk"].sum() * 0.2
            - group_df[group_df["reachabilityScore"] < 0]["risk"].sum() * 0.2
        )

        return pd.Series(
            {
                "severity_weighted_score": severity_weighted_score,
                "kev_exploit_bonus": kev_exploit_bonus,
                "epss_bonus": epss_bonus,
                "reachability_multiplier": reachability_multiplier,
            }
        )

    # Apply severity calculations
    severity_scores = (
        df.groupby(["name", "version"]).apply(calculate_severity_risks).reset_index()
    )

    # Merge back with grouped data
    result = grouped.merge(severity_scores, on=["name", "version"], how="left")

    # Fill NaN values
    result = result.fillna(0)

    # Calculate portfolio composite risk
    result["portfolio_composite_risk"] = (
        result["base_risk_score"]
        + result["severity_weighted_score"]
        + result["kev_exploit_bonus"]
        + result["epss_bonus"]
        + result["reachability_multiplier"]
    )

    # Calculate normalized risk score (guard division by zero)
    result["normalized_risk_score"] = (
        result["portfolio_composite_risk"] / result["project_count"].replace(0, 1)
    ).replace([np.inf, -np.inf], 0)

    # Clean up project_names to be a readable list
    result["project_names"] = result["project_names"].apply(
        lambda x: ", ".join(x) if isinstance(x, list) else str(x)
    )

    return result


def apply_project_filter(df: pd.DataFrame, project_filter: str) -> pd.DataFrame:
    """
    Apply project filtering based on filter type detection.

    Args:
        df: Findings DataFrame
        project_filter: Filter string (project name, ID, or version ID)

    Returns:
        Filtered DataFrame
    """
    if not project_filter or project_filter == "all":
        return df

    # Handle multiple projects (comma-separated)
    if "," in project_filter:
        project_list = [p.strip() for p in project_filter.split(",")]
        filtered_dfs = []
        for project in project_list:
            filtered_df = apply_single_project_filter(df, project)
            filtered_dfs.append(filtered_df)
        return pd.concat(filtered_dfs, ignore_index=True)

    return apply_single_project_filter(df, project_filter)


def apply_single_project_filter(df: pd.DataFrame, project_filter: str) -> pd.DataFrame:
    """
    Apply filtering for a single project identifier.
    """
    try:
        project_id = int(project_filter)
        # Check if it's a project ID — compare as strings to handle mixed types
        if "project.id" in df.columns:
            project_match = df[df["project.id"].astype(str) == str(project_id)]
            if not project_match.empty:
                return project_match
        return pd.DataFrame()
    except ValueError:
        # Not an integer, treat as project name (case-insensitive)
        if "project.name" in df.columns:
            project_match = df[df["project.name"].str.lower() == project_filter.lower()]
            return project_match
        return pd.DataFrame()
