import argparse
import json
import sys
from pathlib import Path
from typing import Any

import numpy as np
import pandas as pd

from featureleak.scanner import LeakScanner


def _load_csv(path: str) -> pd.DataFrame:
    csv_path = Path(path)
    if not csv_path.exists():
        raise FileNotFoundError(f"CSV file not found: {csv_path}")
    return pd.read_csv(csv_path)


def _to_serializable(obj: Any) -> Any:
    """Convert objects to JSON-serializable types."""
    # Handle objects with to_dict method
    if hasattr(obj, "to_dict") and callable(getattr(obj, "to_dict")):
        return obj.to_dict()
    
    if isinstance(obj, dict):
        return {str(k): _to_serializable(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [_to_serializable(v) for v in obj]
    if isinstance(obj, tuple):
        return [_to_serializable(v) for v in obj]
    
    # Handle null values
    if pd.isna(obj) if not isinstance(obj, (list, dict, tuple)) else False:
        return None
    
    # Handle numpy types
    if hasattr(obj, "item"):
        try:
            return obj.item()
        except Exception:
            pass
    
    if isinstance(obj, (np.integer, np.floating)):
        return obj.item()
    if isinstance(obj, np.ndarray):
        return obj.tolist()
    
    return obj


def _write_json_report(report: Any, output_path: str) -> None:
    """Write report to JSON file."""
    path = Path(output_path)
    path.parent.mkdir(parents=True, exist_ok=True)
    
    # Convert LeakReport to dict structure
    if hasattr(report, "issues"):
        report_dict = {
            "issues": [issue.to_dict() for issue in report.issues],
            "metadata": report.metadata,
            "high_risk_features": report.high_risk_features,
            "medium_risk_features": report.medium_risk_features,
            "low_risk_features": report.low_risk_features,
        }
    else:
        report_dict = report
    
    with open(path, "w", encoding="utf-8") as f:
        json.dump(_to_serializable(report_dict), f, indent=2, ensure_ascii=False)


def _print_text_report(report) -> None:
    """
    Print a clean terminal-friendly report.
    Assumes `report` is a LeakReport-like object with:
      - issues
      - metadata
      - high_risk_features / medium_risk_features / low_risk_features
    """

    if report is None:
        print("\nNo report returned.")
        return

    issues = getattr(report, "issues", []) or []
    metadata = getattr(report, "metadata", {}) or {}
    high_risk_features = getattr(report, "high_risk_features", []) or []
    medium_risk_features = getattr(report, "medium_risk_features", []) or []
    low_risk_features = getattr(report, "low_risk_features", []) or []

    score = metadata.get("score")
    risk_level = metadata.get("risk_level")
    total_issues = metadata.get("n_issues", len(issues))

    print("\nFeatureLeak Report")
    print("────────────────────────────────────────────────────────")

    if score is not None and risk_level is not None:
        print(f"Risk score: {score}/100 ({str(risk_level).title()})")
    print(f"Total issues: {total_issues}")
    print(f"High risk features: {len(high_risk_features)}")
    print(f"Medium risk features: {len(medium_risk_features)}")
    print(f"Low risk features: {len(low_risk_features)}")

    if not issues:
        print("\nNo leakage issues detected.")
        return

    print("\nAll issues:\n")

    for i, issue in enumerate(issues, start=1):
        severity = getattr(issue, "severity", "unknown").upper()
        check = getattr(issue, "check", "unknown_check")
        title = getattr(issue, "title", "")
        summary = getattr(issue, "summary", "")
        affected_columns = getattr(issue, "affected_columns", []) or []
        details = getattr(issue, "details", {}) or {}
        suggested_fix = getattr(issue, "suggested_fix", "")

        print(f"{i}. [{severity}] {check}")

        if title:
            print(f"   Title: {title}")

        if summary:
            print(f"   Summary: {summary}")

        if affected_columns:
            print(f"   Columns: {', '.join(map(str, affected_columns))}")

        if details:
            print("   Details:")
            for key, value in details.items():
                print(f"     - {key}: {value}")

        if suggested_fix:
            print(f"   Suggested fix: {suggested_fix}")

        print()

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="featureleak",
        description="FeatureLeak — detect common ML data leakage issues from CSV files.",
    )

    subparsers = parser.add_subparsers(dest="command")

    scan_parser = subparsers.add_parser(
        "scan",
        help="Run a leakage scan against a CSV dataset.",
    )

    scan_parser.add_argument(
        "csv",
        help="Path to the main dataset CSV file.",
    )
    scan_parser.add_argument(
        "--target",
        required=True,
        help="Name of the target column.",
    )
    scan_parser.add_argument(
        "--time-col",
        default=None,
        help="Name of the time column.",
    )
    scan_parser.add_argument(
        "--train",
        default=None,
        help="Optional path to a train split CSV.",
    )
    scan_parser.add_argument(
        "--test",
        default=None,
        help="Optional path to a test split CSV.",
    )
    scan_parser.add_argument(
        "--output",
        default=None,
        help="Path to save the JSON report.",
    )
    scan_parser.add_argument(
        "--corr-threshold",
        type=float,
        default=0.98,
        help="Target correlation threshold for detecting target leakage (default: 0.98).",
    )
    scan_parser.add_argument(
        "--overlap-threshold",
        type=float,
        default=0.0,
        help="Allowed train-test overlap threshold (default: 0.0).",
    )
    scan_parser.add_argument(
        "--sample-size",
        type=int,
        default=None,
        help="Sample size for large datasets (default: None, use all rows).",
    )
    scan_parser.add_argument(
        "--quiet",
        action="store_true",
        help="Suppress info messages and only print report output.",
    )

    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    if args.command != "scan":
        parser.print_help()
        sys.exit(1)

    try:
        if not args.quiet:
            print(f"Loading main dataset: {args.csv}")
        df = _load_csv(args.csv)

        train_df = None
        test_df = None

        if args.train:
            if not args.quiet:
                print(f"Loading train split: {args.train}")
            train_df = _load_csv(args.train)

        if args.test:
            if not args.quiet:
                print(f"Loading test split: {args.test}")
            test_df = _load_csv(args.test)

        scanner = LeakScanner(
            target_corr_threshold=args.corr_threshold,
            overlap_threshold=args.overlap_threshold,
            sample_size=args.sample_size,
        )

        report = scanner.analyse(
            df=df,
            target=args.target,
            time_col=args.time_col,
            train_df=train_df,
            test_df=test_df,
        )

        _print_text_report(report)

        if args.output:
            _write_json_report(report, args.output)
            if not args.quiet:
                print(f"\nSaved JSON report to: {args.output}")

    except KeyboardInterrupt:
        print("\nScan cancelled.")
        sys.exit(130)
    except Exception as e:
        print(f"\nFeatureLeak CLI failed: {type(e).__name__}: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()