# -*- coding: utf-8 -*-
import datetime
from warnings import warn
from pathlib import Path
import argparse
import os
import sys

from automar.shared.persistence.library import (
    date_ender,
    Periods,
    ValidFormats,
    VALID_INDUSTRY,
)

# Import runner functions
from automar.shared.runners import (
    run_extraction,
    run_pca,
    run_tuning,
    run_training,
    run_crossvalidation,
    run_prediction,
    run_api,
    run_gui,
    has_web_ui,
)

# Import utility functions
from automar.shared.services.file_utils import load_module_with_functions


def valid_date(date_string):
    try:
        return datetime.datetime.strptime(date_string, "%Y-%m-%d").date()
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"Invalid date format: {date_string}. Use yyyy-mm-dd format."
        )


def add_extract_arguments(parser):
    group = parser.add_argument_group("Data Extraction Options")

    # Ticker and industry arguments - can be used together or separately
    group.add_argument(
        "--ticker", type=str, help="Ticker symbol to extract data for (e.g., AAPL)."
    )
    group.add_argument(
        "--industry",
        type=str,
        help="Industry name to extract data for (e.g., Healthcare). Must be one of the following: "
        + ", ".join(VALID_INDUSTRY),
    )

    # Optional arguments
    group.add_argument(
        "--history",
        type=Periods,
        default=Periods.Y10,
        choices=Periods,
        help="Historical data period. Choose from: "
        + ", ".join(Periods)
        + f' (default: { "10y" }); ignored if a period is defined by inputting'
        + '"datest"',
    )

    # Boolean flags: --force to re-download even if data exists
    group.add_argument(
        "--force",
        dest="force",
        action="store_true",
        help="Force re-download even if data already exists. (default: False)",
    )
    group.set_defaults(force=False)

    group.add_argument(
        "--dir_path",
        type=Path,
        default=None,
        help="Path to save/find data. Defaults to current directory.",
    )

    group.add_argument(
        "--extract_file",
        type=Path,
        default=None,
        help="Path of the file to read/save the extracted data.",
    )

    group.add_argument(
        "--format",
        type=str,
        default="feather",
        choices=ValidFormats,
        help="Output format of the extracted data. Choose from: "
        + ", ".join(ValidFormats)
        + " (default: feather)",
    )

    group.add_argument(
        "--datest",
        type=valid_date,
        default=None,
        help="Start date in yyyy-mm-dd format.",
    )

    group.add_argument(
        "--datend",
        type=valid_date,
        default=datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d"),
        help="End date in yyyy-mm-dd format. Defaults to the current date if running after 15 PM UTC."
        + "If modified, requires --datest to be specified as well.",
    )

    return parser


def add_pca_arguments(parser):
    group = parser.add_argument_group("PCA Options")

    group.add_argument(
        "--n_components",
        type=int,
        default=0,
        help="Number of components to retain during the PCA. Defaults to 0, which includes all.",
    )

    group.add_argument(
        "--alpha",
        type=float,
        default=0.05,
        help="Significance level for the test during the PCA. Defaults to 0.05.",
    )

    group.add_argument(
        "--drop",
        type=bool,
        default=True,
        help="If True, PCA will discard all non-significant components. Defaults to True.",
    )

    group.add_argument(
        "--data_file",
        type=Path,
        default=None,
        help="Path of the file to load base data from and perform the function operations on.",
    )

    group.add_argument(
        "--pca_file",
        type=Path,
        default=None,
        help="Path of the file to save the PCA object.",
    )

    group.add_argument(
        "--pca_force",
        action="store_true",
        help="Flag to force recomputing the PCA, even if an existing file is found. Defaults to False.",
    )

    group.add_argument(
        "--notdf",
        action="store_true",
        help="Do not save the transformed data generated by the pca.",
    )

    group.add_argument(
        "--pca_df_file",
        type=Path,
        default=None,
        help="If defined, save the transformed dataframe in the specified file.",
    )

    return parser


def add_loaders_arguments(parser):
    import multiprocessing
    from automar.shared.config.schemas import SCALERS_KEYS

    available_cores = multiprocessing.cpu_count()
    default_cores = max(1, min(available_cores // 2, 4))
    group = parser.add_argument_group("Loader building options")

    group.add_argument(
        "--tsize", type=int, default=20, help="Time size parameter for loader"
    )

    group.add_argument(
        "--batch_size", type=int, default=50, help="Batch size for data loaders"
    )

    group.add_argument(
        "--val_size", type=float, default=0.15, help="Validation set size ratio"
    )

    group.add_argument(
        "--test_size", type=float, default=0.15, help="Test set size ratio"
    )

    group.add_argument(
        "--nopca",
        dest="dopca",
        action="store_false",
        help="Disable PCA during batch preparation when no PCA file is loaded. By default, PCA is performed for each batch of data while preparing the loaders.",
    )

    group.add_argument(
        "--device",
        type=str.lower,
        default=None,
        choices=["cpu", "cuda", "mps", "xpu"],
        help=(
            "Device to run on (cpu, cuda, mps, or xpu). "
            "If not specified, will auto-detect best available device."
        ),
    )

    group.add_argument(
        "--cores",
        type=int,
        default=default_cores,
        choices=range(1, available_cores + 1),
        metavar=f"1-{available_cores}",
        help=f'Number of CPU cores to use when device="cpu" (defaults to {default_cores}, max {available_cores} available)',
    )

    group.add_argument(
        "--seed", type=int, default=2, help="Random seed for reproducibility"
    )

    group.add_argument(
        "--scaler",
        type=str,
        default="standard",
        choices=SCALERS_KEYS,
        help="Scaler applied to the data.",
    )
    return parser


def add_tuning_arguments(parser):
    group = parser.add_argument_group("Hyperparameter tuning options")

    group.add_argument(
        "--tuning_path",
        type=Path,
        default=None,
        help="Path to the module with search space function",
    )

    group.add_argument(
        "--param_path",
        type=str,
        default="out/hyper",
        help="Path to save the tuning results",
    )

    group.add_argument(
        "--num_samples",
        type=int,
        default=50,
        help="Number of experiments for hyperparameter tuning",
    )

    group.add_argument(
        "--epochs",
        type=int,
        default=50,
        help="Number of times to run a specific hyperparameter setup.",
    )

    group.add_argument(
        "--gpu_per_trial",
        type=float,
        default=1,
        help="Fraction of GPU to allocate per trial (0.25 means four trials run in parallel, 1 means a single trial at a time)",
    )

    group.add_argument(
        "--model",
        type=str,
        default="GRU",
        choices=["GRU", "transformer", "log-reg"],
        help="Model to tune for: GRU, transformer or logistic regression",
    )

    return parser


def add_training_arguments(parser):
    group = parser.add_argument_group("Model training options")

    group.add_argument(
        "--cfg_path",
        type=Path,
        default=None,
        help="Path to the file defining the hyperparameters",
    )

    group.add_argument(
        "--pca_path",
        type=Path,
        default=None,
        help="[OPTIONAL] Path to the pre-fitted PCA file to transform the training data with, generated by the 'pca' function.",
    )

    group.add_argument(
        "--model_path",
        type=Path,
        default=None,
        help="Path to store the trained model in",
    )

    group.add_argument(
        "--id",
        type=int,
        default=1,
        help="Identification number of the set of tuned hyperparameters to load, used when more than one configuration has been generated for the same set of data and model.",
    )


def add_crossvalidate_arguments(parser):
    group = parser.add_argument_group("Crossvalidation options")

    group.add_argument(
        "--out_path",
        type=Path,
        default=None,
        help="Path to store the crossvalidation results.",
    )

    group.add_argument(
        "--n_split",
        type=int,
        default=5,
        help="Number of splits or growing windows for cross-validation",
    )


def add_prediction_arguments(parser):
    group = parser.add_argument_group("Prediction options")

    group.add_argument(
        "--load_model",
        type=str,
        required=True,
        dest="model_path",
        help="Full path to trained model file (.pth) including filename (required)",
    )

    group.add_argument(
        "--pred_dir",
        type=Path,
        default=None,
        dest="save_dir",
        help="Directory to save prediction results (default: out/preds/)",
    )

    group.add_argument(
        "--mode",
        type=str,
        choices=["eval", "forecast"],
        default="eval",
        help="Prediction mode: 'eval' for test set evaluation (default), 'forecast' for future predictions",
    )


def create_extract_parser(subparsers):
    parser = subparsers.add_parser(
        "extract", help="Extract data based on ticker, industry, or both."
    )
    add_extract_arguments(parser)

    return parser


def create_pca_parser(subparsers):
    parser = subparsers.add_parser(
        "pca", help="Perform Principal Component Analysis (PCA) on a dataset."
    )
    add_extract_arguments(parser)
    add_pca_arguments(parser)

    return parser


def create_tuning_parser(subparsers):
    parser = subparsers.add_parser("tune", help="Hyperparameter tuning parser.")

    add_extract_arguments(parser)
    add_pca_arguments(parser)
    add_loaders_arguments(parser)
    add_tuning_arguments(parser)

    return parser


def create_training_parser(subparsers):
    parser = subparsers.add_parser("train", help="Training parser")

    add_extract_arguments(parser)
    add_pca_arguments(parser)
    add_loaders_arguments(parser)
    add_tuning_arguments(parser)
    add_training_arguments(parser)

    return parser


def create_crossvalidate_parser(subparser):
    parser = subparser.add_parser("crossvalidate", help="Crossvalidation parser")

    add_extract_arguments(parser)
    add_pca_arguments(parser)
    add_loaders_arguments(parser)
    add_tuning_arguments(parser)
    add_training_arguments(parser)
    add_crossvalidate_arguments(parser)

    return parser


def create_predict_parser(subparsers):
    parser = subparsers.add_parser(
        "predict", help="Run inference using a trained model"
    )

    add_extract_arguments(parser)
    add_pca_arguments(parser)
    add_loaders_arguments(parser)
    add_tuning_arguments(parser)
    add_training_arguments(parser)
    add_prediction_arguments(parser)

    return parser


def create_api_parser(subparsers):
    """Create the API server subcommand parser"""
    parser = subparsers.add_parser(
        "api", help="Run the FastAPI server for REST API access"
    )

    parser.add_argument(
        "--host",
        type=str,
        default="127.0.0.1",
        help="Host to bind the server to (default: 127.0.0.1)",
    )

    parser.add_argument(
        "--port",
        type=int,
        default=8000,
        help="Port to bind the server to (default: 8000)",
    )

    parser.add_argument(
        "--reload", action="store_true", help="Enable auto-reload for development"
    )

    parser.add_argument(
        "--workers",
        type=int,
        default=1,
        help="Number of worker processes (default: 1, use > 1 for production)",
    )

    return parser


def create_gui_parser(subparsers):
    """Create the GUI subcommand parser (only if web UI is available)"""
    parser = subparsers.add_parser("gui", help="Start web UI (API server + browser)")

    parser.add_argument(
        "--host",
        type=str,
        default="127.0.0.1",
        help="Host to bind the server to (default: 127.0.0.1)",
    )

    parser.add_argument(
        "--port",
        type=int,
        default=8000,
        help="Port to bind the server to (default: 8000)",
    )

    parser.add_argument(
        "--reload", action="store_true", help="Enable auto-reload for development"
    )

    parser.add_argument(
        "--workers",
        type=int,
        default=1,
        help="Number of worker processes (default: 1, use > 1 for production)",
    )

    return parser


def create_gap_analysis_parser(subparsers):
    """Create gap-analysis subcommand parser"""
    parser = subparsers.add_parser(
        "gap-analysis", help="Analyze missing data in SQLite database"
    )

    parser.add_argument(
        "--industry",
        type=str,
        required=True,
        help="Industry name to analyze (e.g., 'Information Technology'). Must be one of the following: "
        + ", ".join(VALID_INDUSTRY),
    )

    parser.add_argument(
        "--start-date",
        type=valid_date,
        required=True,
        help="Start date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--end-date",
        type=valid_date,
        required=True,
        help="End date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--db-path",
        type=Path,
        default=None,
        help="Path to SQLite database file (default: out/data/data.sqlite)",
    )

    parser.add_argument(
        "--export-json",
        type=Path,
        default=None,
        help="Export gap analysis results to JSON file",
    )

    parser.add_argument(
        "--export-csv",
        type=Path,
        default=None,
        help="Export per-company gap details to CSV file",
    )

    parser.add_argument(
        "--export-markdown",
        type=Path,
        default=None,
        help="Export detailed markdown report",
    )

    parser.add_argument(
        "--compact",
        action="store_true",
        help="Print compact one-line summary instead of detailed report",
    )

    return parser


def create_gap_report_parser(subparsers):
    """Create gap-report subcommand parser for all industries"""
    parser = subparsers.add_parser(
        "gap-report", help="Generate gap analysis report for all industries"
    )

    parser.add_argument(
        "--start-date",
        type=valid_date,
        required=True,
        help="Start date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--end-date",
        type=valid_date,
        required=True,
        help="End date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--db-path",
        type=Path,
        default=None,
        help="Path to SQLite database file (default: out/data/data.sqlite)",
    )

    parser.add_argument(
        "--export-dir",
        type=Path,
        default=None,
        help="Directory to export individual industry reports (JSON and markdown)",
    )

    return parser


def create_download_missing_parser(subparsers):
    """Create download-missing subcommand parser"""
    parser = subparsers.add_parser(
        "download-missing",
        help="Download missing companies for an industry (Phase 3 - Unified Acquisition)",
    )

    parser.add_argument(
        "--industry",
        type=str,
        required=True,
        help="Industry name (e.g., 'Information Technology'). Must be one of: "
        + ", ".join(VALID_INDUSTRY),
    )

    parser.add_argument(
        "--start-date",
        type=valid_date,
        required=True,
        help="Start date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--end-date",
        type=valid_date,
        required=True,
        help="End date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--db-path",
        type=Path,
        default=None,
        help="Path to SQLite database file (default: out/data/data.sqlite)",
    )

    parser.add_argument(
        "--no-confirm",
        action="store_true",
        help="Skip user confirmation prompt",
    )

    return parser


def create_update_industry_parser(subparsers):
    """Create update-industry subcommand parser"""
    parser = subparsers.add_parser(
        "update-industry",
        help="Update industry data to target date (Phase 3 - Unified Acquisition)",
    )

    parser.add_argument(
        "--industry",
        type=str,
        required=True,
        help="Industry name (e.g., 'Information Technology'). Must be one of: "
        + ", ".join(VALID_INDUSTRY),
    )

    parser.add_argument(
        "--target-date",
        type=valid_date,
        default=None,
        help="Target date to update to (default: today)",
    )

    parser.add_argument(
        "--db-path",
        type=Path,
        default=None,
        help="Path to SQLite database file (default: out/data/data.sqlite)",
    )

    return parser


def create_backfill_gaps_parser(subparsers):
    """Create backfill-gaps subcommand parser"""
    parser = subparsers.add_parser(
        "backfill-gaps",
        help="Fill date gaps for companies with incomplete data (Phase 3 - Unified Acquisition)",
    )

    parser.add_argument(
        "--industry",
        type=str,
        required=True,
        help="Industry name (e.g., 'Information Technology'). Must be one of: "
        + ", ".join(VALID_INDUSTRY),
    )

    parser.add_argument(
        "--start-date",
        type=valid_date,
        required=True,
        help="Start date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--end-date",
        type=valid_date,
        required=True,
        help="End date in yyyy-mm-dd format",
    )

    parser.add_argument(
        "--db-path",
        type=Path,
        default=None,
        help="Path to SQLite database file (default: out/data/data.sqlite)",
    )

    return parser


def create_main_parser():
    main_parser = argparse.ArgumentParser(
        description="Data Extraction and Processing Tool"
    )

    main_parser.add_argument(
        "-c",
        "--config",
        dest="config",
        type=Path,
        help="Path to TOML config file with defaults for every command",
    )

    subparsers = main_parser.add_subparsers(
        title="Commands",
        description="Available commands",
        help="Description",
        dest="command",
        required=True,
    )
    subparsers.required = True

    # Extract Subparser
    create_extract_parser(subparsers)

    # PCA Subparser
    create_pca_parser(subparsers)

    # Tuning Subparser
    create_tuning_parser(subparsers)

    # Training Subparser
    create_training_parser(subparsers)

    # Crossvalidate Subparser
    create_crossvalidate_parser(subparsers)

    # Prediction Subparser
    create_predict_parser(subparsers)

    # API Subparser
    create_api_parser(subparsers)

    # GUI Subparser (only if web UI is available)
    if has_web_ui():
        create_gui_parser(subparsers)

    # Gap Analysis Subparsers
    create_gap_analysis_parser(subparsers)
    create_gap_report_parser(subparsers)

    # Data Acquisition Subparsers (Phase 3)
    create_download_missing_parser(subparsers)
    create_update_industry_parser(subparsers)
    create_backfill_gaps_parser(subparsers)

    return main_parser


def run_gap_analysis(args):
    """Execute gap analysis command"""
    from automar.shared.services.gap_analyzer import GapAnalyzer
    from automar.shared.services.gap_reporter import GapReporter
    from automar.shared.config.path_resolver import get_output_dir

    # Determine database path
    if args.db_path:
        db_path = Path(args.db_path)
    else:
        db_path = get_output_dir("data") / "data.sqlite"

    if not db_path.exists():
        print(f"Error: Database file not found at {db_path}")
        sys.exit(1)

    # Create analyzer and run analysis
    analyzer = GapAnalyzer(db_path)
    result = analyzer.analyze_industry(
        industry=args.industry,
        start_date=args.start_date,
        end_date=args.end_date,
    )

    # Print results
    if args.compact:
        GapReporter.print_compact_summary(result)
    else:
        GapReporter.print_summary(result)

    # Export if requested
    if args.export_json:
        GapReporter.export_to_json(result, args.export_json)
        print(f"\nExported JSON to {args.export_json}")

    if args.export_csv:
        GapReporter.export_to_csv(result.company_specific_gaps, args.export_csv)
        print(f"Exported CSV to {args.export_csv}")

    if args.export_markdown:
        GapReporter.generate_markdown_report(result, args.export_markdown)
        print(f"Exported markdown report to {args.export_markdown}")


def run_gap_report(args):
    """Execute gap report command for all industries"""
    from automar.shared.services.gap_analyzer import GapAnalyzer
    from automar.shared.services.gap_reporter import GapReporter
    from automar.shared.config.path_resolver import get_output_dir

    # Determine database path
    if args.db_path:
        db_path = Path(args.db_path)
    else:
        db_path = get_output_dir("data") / "data.sqlite"

    if not db_path.exists():
        print(f"Error: Database file not found at {db_path}")
        sys.exit(1)

    # Create analyzer and run analysis for all industries
    analyzer = GapAnalyzer(db_path)
    results = analyzer.analyze_all_industries(
        start_date=args.start_date,
        end_date=args.end_date,
    )

    # Print multi-industry summary
    GapReporter.print_multiple_industries(results)

    # Export if requested
    if args.export_dir:
        export_dir = Path(args.export_dir)
        export_dir.mkdir(parents=True, exist_ok=True)

        for industry, result in results.items():
            # Sanitize industry name for filename
            safe_name = industry.replace(" ", "_").replace("/", "_")

            # Export JSON
            json_path = export_dir / f"{safe_name}_gap_analysis.json"
            GapReporter.export_to_json(result, json_path)

            # Export markdown
            md_path = export_dir / f"{safe_name}_gap_report.md"
            GapReporter.generate_markdown_report(result, md_path)

        print(f"\nExported reports to {export_dir}/")


def run_download_missing(args):
    """Execute download-missing command"""
    from automar.shared.services.data_acquisition import download_missing_companies
    from automar.shared.config.path_resolver import get_output_dir

    # Determine database path
    if args.db_path:
        db_path = Path(args.db_path)
    else:
        db_path = get_output_dir("data") / "data.sqlite"

    if not db_path.exists():
        print(f"❌ Error: Database file not found at {db_path}")
        print(
            "Create a database first using the 'extract' command with --format sqlite"
        )
        sys.exit(1)

    # Execute download
    result = download_missing_companies(
        db_file=db_path,
        industry=args.industry,
        start_date=args.start_date,
        end_date=args.end_date,
        no_confirm=args.no_confirm,
    )

    # Print summary
    print(f"\n{result.summary()}")

    if not result.success:
        sys.exit(1)


def run_update_industry(args):
    """Execute update-industry command"""
    from automar.shared.services.data_acquisition import update_industry_to_date
    from automar.shared.config.path_resolver import get_output_dir

    # Determine database path
    if args.db_path:
        db_path = Path(args.db_path)
    else:
        db_path = get_output_dir("data") / "data.sqlite"

    if not db_path.exists():
        print(f"❌ Error: Database file not found at {db_path}")
        print(
            "Create a database first using the 'extract' command with --format sqlite"
        )
        sys.exit(1)

    # Execute update
    result = update_industry_to_date(
        db_file=db_path,
        industry=args.industry,
        target_date=args.target_date,
    )

    # Print summary
    print(f"\n{result.summary()}")

    if not result.success:
        sys.exit(1)


def run_backfill_gaps(args):
    """Execute backfill-gaps command"""
    from automar.shared.services.data_acquisition import backfill_all_gaps
    from automar.shared.config.path_resolver import get_output_dir

    # Determine database path
    if args.db_path:
        db_path = Path(args.db_path)
    else:
        db_path = get_output_dir("data") / "data.sqlite"

    if not db_path.exists():
        print(f"❌ Error: Database file not found at {db_path}")
        print(
            "Create a database first using the 'extract' command with --format sqlite"
        )
        sys.exit(1)

    # Execute backfill
    result = backfill_all_gaps(
        db_file=db_path,
        industry=args.industry,
        start_date=args.start_date,
        end_date=args.end_date,
    )

    # Print summary
    print(f"\n{result.summary()}")

    if not result.success:
        sys.exit(1)


def main() -> None:
    from pathlib import Path
    from automar.shared.config.config_utils import from_cli, from_toml, merge

    parser = create_main_parser()
    args = parser.parse_args()

    cli_cfg = from_cli(args)
    file_cfg = None
    if args.config:
        file_cfg = from_toml(args.config)

    cfg = merge(cli_cfg, file_cfg)

    match cfg.command:
        case "extract":
            run_extraction(cfg.extract)
        case "pca":
            run_pca(cfg)
        case "tune":
            run_tuning(cfg)
        case "train":
            run_training(cfg)
        case "crossvalidate":
            run_crossvalidation(cfg)
        case "predict":
            run_prediction(cfg)
        case "api":
            run_api(cfg)
        case "gui":
            run_gui(cfg)
        case "gap-analysis":
            run_gap_analysis(args)
        case "gap-report":
            run_gap_report(args)
        case "download-missing":
            run_download_missing(args)
        case "update-industry":
            run_update_industry(args)
        case "backfill-gaps":
            run_backfill_gaps(args)


if __name__ == "__main__":
    a, c = main()
