from __future__ import annotations

import argparse
import json
import os
import tempfile
import webbrowser
from pathlib import Path

from auraindex.pipeline import ScanConfig, run_scan
from auraindex.reporting import (
    build_report_dict,
    render_ddl,
    render_html_report,
    render_markdown_report,
    render_migration,
    render_orm_snippets,
)


def main(argv: list[str] | None = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    if args.limit_suggestions is not None and args.limit_suggestions < 1:
        parser.error("--limit-suggestions must be >= 1.")
    if args.max_parse_failures < 1:
        parser.error("--max-parse-failures must be >= 1.")

    cardinality_hints = _load_cardinality_hints(args.cardinality_hints)
    report_dir = _resolve_report_dir(args.report_dir)
    json_out = _resolve_output_path(report_dir, args.json_out, "auraindex_report.json")
    ddl_out = _resolve_output_path(report_dir, args.ddl_out, "auraindex_indexes.sql")
    orm_out = _resolve_output_path(report_dir, args.orm_out, "auraindex_orm_snippets.md")
    md_out = _resolve_output_path(report_dir, args.md_out, "auraindex_report.md")
    html_out = _resolve_output_path(report_dir, args.html_out, "auraindex_report.html")
    migration_out = _resolve_output_path(report_dir, args.migration_out, "auraindex_migration.sql")

    config = ScanConfig(
        target_path=Path(args.path),
        dialect=args.dialect,
        extensions=_parse_csv_set(args.extensions, normalize_extension=True),
        exclude_dirs=_parse_csv_set(args.exclude_dirs, normalize_extension=False),
        enable_wasm=not args.disable_wasm,
        min_support=args.min_support,
        max_indexes_per_table=args.max_indexes_per_table,
        cardinality_hints=cardinality_hints,
        enable_impact_scoring=not args.disable_impact_scoring,
        enable_index_type_hints=not args.disable_index_type_hints,
        enable_schema_checks=not args.disable_schema_checks,
        enable_drift_watch=not args.disable_drift_watch,
        enable_query_advisories=not args.disable_query_advisories,
        max_query_advisories=args.max_query_advisories,
        enable_db_introspection=args.enable_db_introspection,
        db_dialect=args.db_dialect,
        db_host=args.db_host,
        db_port=args.db_port,
        db_user=args.db_user,
        db_password=_resolve_db_password(args.db_password, args.db_password_env),
        db_name=args.db_name,
        db_path=Path(args.db_path) if args.db_path else None,
        db_schema=args.db_schema,
        db_timeout_seconds=args.db_timeout_seconds,
        explain_validate=args.explain_validate,
        explain_db_path=Path(args.explain_db_path) if args.explain_db_path else None,
        explain_command=args.explain_command,
        explain_timeout_seconds=args.explain_timeout_seconds,
        explain_max_queries=args.explain_max_queries,
    )

    result = run_scan(config)
    report_dict = build_report_dict(
        result,
        dialect=config.dialect,
        config={
            "path": str(config.target_path),
            "extensions": sorted(config.extensions),
            "exclude_dirs": sorted(config.exclude_dirs),
            "enable_wasm": config.enable_wasm,
            "min_support": config.min_support,
            "max_indexes_per_table": config.max_indexes_per_table,
            "cardinality_hints": bool(config.cardinality_hints),
            "enable_impact_scoring": config.enable_impact_scoring,
            "enable_index_type_hints": config.enable_index_type_hints,
            "enable_schema_checks": config.enable_schema_checks,
            "enable_drift_watch": config.enable_drift_watch,
            "enable_query_advisories": config.enable_query_advisories,
            "max_query_advisories": config.max_query_advisories,
            "enable_db_introspection": config.enable_db_introspection,
            "db_dialect": config.db_dialect,
            "db_host": config.db_host,
            "db_port": config.db_port,
            "db_user": config.db_user,
            "db_name": config.db_name,
            "db_path": str(config.db_path) if config.db_path else None,
            "db_schema": config.db_schema,
            "explain_validate": config.explain_validate,
            "explain_db_path": str(config.explain_db_path) if config.explain_db_path else None,
            "explain_command_configured": bool(config.explain_command),
        },
    )

    json_out.write_text(json.dumps(report_dict, indent=2), encoding="utf-8")
    ddl_out.write_text(render_ddl(result), encoding="utf-8")
    orm_out.write_text(render_orm_snippets(result), encoding="utf-8")
    md_out.write_text(
        render_markdown_report(
            result,
            dialect=config.dialect,
            config={
                "path": str(config.target_path),
                "extensions": sorted(config.extensions),
            },
            suggestion_limit=args.limit_suggestions,
            max_parse_failures=args.max_parse_failures,
        ),
        encoding="utf-8",
    )
    if not args.disable_migration_output:
        migration_out.write_text(render_migration(result, dialect=config.dialect), encoding="utf-8")
    html_out.write_text(
        render_html_report(
            result,
            dialect=config.dialect,
            config={
                "path": str(config.target_path),
                "extensions": sorted(config.extensions),
            },
            suggestion_limit=args.limit_suggestions,
            max_parse_failures=args.max_parse_failures,
        ),
        encoding="utf-8",
    )

    print(
        "AuraIndex scan complete: "
        f"files={len(result.scanned_files)}, "
        f"candidates={len(result.candidates)}, "
        f"parsed={len(result.parsed_queries)}, "
        f"suggestions={len(result.suggestions)}"
    )
    if result.parse_failures:
        print(f"Parse failures: {len(result.parse_failures)} (see {json_out})")
    if result.warnings:
        print(f"Warnings: {len(result.warnings)} (see {json_out})")
    print(f"Report directory: {report_dir}")
    print(f"Report: {json_out}")
    print(f"DDL: {ddl_out}")
    print(f"ORM snippets: {orm_out}")
    print(f"Detailed markdown: {md_out}")
    if not args.disable_migration_output:
        print(f"Migration SQL (up/down): {migration_out}")
    print(f"Interactive HTML: {html_out}")
    opened = _open_html_report(html_out)
    if opened:
        print(f"Opened in browser: {html_out.as_uri()}")
    else:
        print(f"Could not auto-open browser. Open manually: {html_out.as_uri()}")

    if args.show_top and result.suggestions:
        shown_suggestions = _apply_suggestion_limit(result.suggestions, args.limit_suggestions)
        print("\nRanked index suggestions:")
        for rank, suggestion in enumerate(shown_suggestions, start=1):
            print(
                f"{rank}. {suggestion.index_name} ({suggestion.table}: {', '.join(suggestion.columns)}) "
                f"score={suggestion.score:.2f}, confidence={suggestion.confidence:.2f}"
            )
        if args.limit_suggestions is not None and len(shown_suggestions) < len(result.suggestions):
            print(
                f"Showing {len(shown_suggestions)} of {len(result.suggestions)} suggestions "
                "because --limit-suggestions is set."
            )

    if args.strict_parse and result.parse_failures:
        print("Strict parse mode enabled and parse failures were found.")
        return 2

    return 0


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="auraindex",
        usage="auraindex [scan options] [report options] [recommendation options] [db options] [explain options] [display options]",
        description="AST-based SQL pattern extraction and index recommendation engine.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    scan_group = parser.add_argument_group("Scan target and extraction")
    scan_group.add_argument("--path", default=".", help="Target codebase path to scan.")
    scan_group.add_argument(
        "--dialect",
        default="postgres",
        choices=["postgres", "mysql", "sqlite"],
        help="SQL dialect used for parsing and DDL generation.",
    )
    scan_group.add_argument(
        "--extensions",
        default=".py,.sql,.js,.jsx,.ts,.tsx,.php",
        help="Comma-separated file extensions to scan.",
    )
    scan_group.add_argument(
        "--exclude-dirs",
        default=".git,node_modules,__pycache__,.venv,venv,build,dist,staticfiles,.next",
        help="Comma-separated directory names to exclude.",
    )
    scan_group.add_argument(
        "--disable-wasm",
        action="store_true",
        help="Disable Tree-sitter WASM extraction (only .sql files will be scanned).",
    )

    output_group = parser.add_argument_group("Report output files")
    output_group.add_argument(
        "--report-dir",
        default=None,
        help=(
            "Directory where reports are written. If omitted, AuraIndex creates a temp folder "
            "like %%TEMP%%\\auraindex-report-*."
        ),
    )
    output_group.add_argument("--json-out", default=None, help="JSON report file name or absolute path.")
    output_group.add_argument("--ddl-out", default=None, help="DDL output file name or absolute path.")
    output_group.add_argument("--orm-out", default=None, help="ORM snippet output file name or absolute path.")
    output_group.add_argument("--md-out", default=None, help="Detailed markdown report file name or absolute path.")
    output_group.add_argument("--html-out", default=None, help="Interactive HTML report file name or absolute path.")
    output_group.add_argument("--migration-out", default=None, help="Migration SQL (up/down) output file name or absolute path.")
    output_group.add_argument(
        "--disable-migration-output",
        action="store_true",
        help="Disable writing migration SQL artifact.",
    )

    recommendation_group = parser.add_argument_group("Recommendation behavior")
    recommendation_group.add_argument("--min-support", type=int, default=2, help="Minimum support threshold for suggestions.")
    recommendation_group.add_argument(
        "--max-indexes-per-table",
        type=int,
        default=4,
        help="Maximum number of index suggestions per table.",
    )
    recommendation_group.add_argument(
        "--cardinality-hints",
        default=None,
        help="Path to JSON file with optional cardinality hints.",
    )
    recommendation_group.add_argument(
        "--disable-impact-scoring",
        action="store_true",
        help="Disable read-vs-write impact scoring adjustments.",
    )
    recommendation_group.add_argument(
        "--disable-index-type-hints",
        action="store_true",
        help="Disable engine-aware index type recommendation metadata.",
    )
    recommendation_group.add_argument(
        "--disable-schema-checks",
        action="store_true",
        help="Disable schema-aware checks against existing indexes.",
    )
    recommendation_group.add_argument(
        "--disable-drift-watch",
        action="store_true",
        help="Disable schema drift watch outputs.",
    )
    recommendation_group.add_argument(
        "--disable-query-advisories",
        action="store_true",
        help="Disable query-level optimization advisories in reports.",
    )
    recommendation_group.add_argument(
        "--max-query-advisories",
        type=int,
        default=120,
        help="Maximum number of query advisories captured in report metadata.",
    )

    db_group = parser.add_argument_group("Live database introspection")
    db_group.add_argument(
        "--enable-db-introspection",
        action="store_true",
        help="Enable live database introspection for schema checks and drift watch.",
    )
    db_group.add_argument(
        "--db-dialect",
        choices=["postgres", "mysql", "sqlite"],
        default=None,
        help="Database dialect for live introspection. Defaults to --dialect when omitted.",
    )
    db_group.add_argument("--db-host", default=None, help="Database host for live introspection.")
    db_group.add_argument("--db-port", type=int, default=None, help="Database port for live introspection.")
    db_group.add_argument("--db-user", default=None, help="Database username for live introspection.")
    db_group.add_argument("--db-password", default=None, help="Database password for live introspection.")
    db_group.add_argument(
        "--db-password-env",
        default=None,
        help="Environment variable name containing database password.",
    )
    db_group.add_argument("--db-name", default=None, help="Database name for live introspection.")
    db_group.add_argument("--db-path", default=None, help="SQLite database file path for live introspection.")
    db_group.add_argument("--db-schema", default=None, help="Database schema filter (currently for postgres).")
    db_group.add_argument(
        "--db-timeout-seconds",
        type=int,
        default=8,
        help="Connection/query timeout for live DB introspection.",
    )

    explain_group = parser.add_argument_group("EXPLAIN validation")
    explain_group.add_argument(
        "--explain-validate",
        action="store_true",
        help="Enable optional EXPLAIN-based validation for suggested indexes.",
    )
    explain_group.add_argument(
        "--explain-db-path",
        default=None,
        help="SQLite database path for built-in EXPLAIN QUERY PLAN validation.",
    )
    explain_group.add_argument(
        "--explain-command",
        default=None,
        help="Custom shell command for EXPLAIN validation. Use '{sql}' placeholder for query injection.",
    )
    explain_group.add_argument(
        "--explain-timeout-seconds",
        type=int,
        default=15,
        help="Timeout per EXPLAIN execution in seconds.",
    )
    explain_group.add_argument(
        "--explain-max-queries",
        type=int,
        default=1,
        help="Maximum matched queries sampled per suggestion for EXPLAIN.",
    )

    display_group = parser.add_argument_group("Display and exit behavior")
    display_group.add_argument(
        "--show-top",
        action="store_true",
        help="Print ranked index suggestions to stdout after scan.",
    )
    display_group.add_argument(
        "--limit-suggestions",
        dest="limit_suggestions",
        type=int,
        default=None,
        help="Limit ranked suggestions in markdown/HTML/--show-top output. By default all are shown.",
    )
    display_group.add_argument(
        "--top-suggestions",
        dest="limit_suggestions",
        type=int,
        help=argparse.SUPPRESS,
    )
    display_group.add_argument(
        "--max-parse-failures",
        type=int,
        default=20,
        help="Maximum number of parse failures shown in markdown report.",
    )
    display_group.add_argument(
        "--strict-parse",
        action="store_true",
        help="Exit with code 2 when parse failures are detected.",
    )
    return parser


def _parse_csv_set(value: str, *, normalize_extension: bool) -> set[str]:
    parsed: set[str] = set()
    for item in value.split(","):
        token = item.strip()
        if not token:
            continue
        if normalize_extension:
            token = token.lower()
            if not token.startswith("."):
                token = f".{token}"
        parsed.add(token)
    return parsed


def _load_cardinality_hints(path_value: str | None) -> dict[str, str] | None:
    if not path_value:
        return None
    path = Path(path_value)
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError("Cardinality hints file must contain a JSON object.")
    result: dict[str, str] = {}
    for key, value in payload.items():
        result[str(key)] = str(value)
    return result


def _resolve_report_dir(report_dir_arg: str | None) -> Path:
    if report_dir_arg:
        report_dir = Path(report_dir_arg)
        report_dir.mkdir(parents=True, exist_ok=True)
        return report_dir.resolve()

    auto_dir = Path(tempfile.mkdtemp(prefix="auraindex-report-"))
    return auto_dir.resolve()


def _resolve_output_path(report_dir: Path, explicit_value: str | None, default_name: str) -> Path:
    if explicit_value:
        candidate = Path(explicit_value)
        if candidate.is_absolute() or candidate.parent != Path("."):
            candidate.parent.mkdir(parents=True, exist_ok=True)
            return candidate.resolve()
        return (report_dir / candidate.name).resolve()
    return (report_dir / default_name).resolve()


def _open_html_report(html_path: Path) -> bool:
    try:
        return bool(webbrowser.open(html_path.resolve().as_uri(), new=2))
    except (webbrowser.Error, OSError, ValueError):
        return False


def _resolve_db_password(password: str | None, password_env: str | None) -> str | None:
    if password:
        return password
    if password_env:
        return os.getenv(password_env)
    return None


def _apply_suggestion_limit(items: list[object], limit: int | None) -> list[object]:
    if limit is None or limit >= len(items):
        return items
    return items[:limit]
