"""Command-line interface for managebac-fetch.

This module turns the scraper into a proper installable CLI tool.

Commands
--------
configure
    Prompt for and store encrypted credentials and default settings.

sync
    Sync files from ManageBac using stored credentials by default.

config
    Inspect or clear saved configuration.

Default behavior
----------------
When the executable is invoked without a subcommand, it behaves like ``sync``.
This makes the package convenient to use via an app launcher or after
installation with pipx.

Examples
--------
managebac-fetch
managebac-fetch sync
managebac-fetch sync --class-name "IB DP Economics 2027  (IB1)"
managebac-fetch configure
managebac-fetch config show
managebac-fetch config clear
"""

from __future__ import annotations

import argparse
import getpass
import signal
from collections.abc import Sequence
from multiprocessing import Pool
from pathlib import Path

from tqdm import tqdm

from . import __version__
from .auth import ManageBacAuthenticationError, login, logout
from .browser import ClassInfo, get_classes, get_files, get_valid_filename
from .config import (
    ConfigError,
    get_config_summary,
    has_saved_credentials,
    load_app_config,
    load_credentials,
    normalize_output_dir,
    validate_output_dir,
)
from .config import (
    configure as save_full_configuration,
)
from .downloader import DownloadSummary, download_files


def build_parser() -> argparse.ArgumentParser:
    """Create and return the top-level argument parser."""
    parser = argparse.ArgumentParser(
        prog="managebac-fetch",
        description=(
            "Sync files from the ManageBac Files area into a local directory."
        ),
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {__version__}",
    )

    subparsers = parser.add_subparsers(dest="command")
    parser.set_defaults(
        school_code=None,
        username=None,
        password=None,
        output_dir=None,
        class_name=None,
        workers=None,
        sync=None,
        quiet_download_progress=False,
        config_command=None,
    )

    configure_parser = subparsers.add_parser(
        "configure",
        help="Save encrypted credentials and default settings",
        description=(
            "Prompt for ManageBac credentials and save encrypted configuration "
            "for future sync runs."
        ),
    )
    configure_parser.add_argument(
        "--school-code",
        help="ManageBac school code, e.g. 'myschool' for https://myschool.managebac.com",
    )
    configure_parser.add_argument(
        "--username",
        help="ManageBac login email address",
    )
    configure_parser.add_argument(
        "--password",
        help="ManageBac login password (not recommended; prompt is safer)",
    )
    configure_parser.add_argument(
        "--output-dir",
        help="Default absolute output directory for synced files",
    )
    configure_parser.add_argument(
        "--workers",
        type=int,
        default=4,
        help="Default number of parallel download workers (default: 4)",
    )
    configure_parser.add_argument(
        "--force",
        dest="sync",
        action="store_false",
        help="Disable sync mode by default and force redownloads on future runs",
    )
    configure_parser.set_defaults(sync=True)

    sync_parser = subparsers.add_parser(
        "sync",
        help="Sync files using stored configuration or explicit arguments",
        description="Sync files from ManageBac into the configured output directory.",
    )
    sync_parser.add_argument(
        "--school-code",
        help="Override stored school code",
    )
    sync_parser.add_argument(
        "--username",
        help="Override stored username",
    )
    sync_parser.add_argument(
        "--password",
        help="Override stored password (not recommended; prompt is safer)",
    )
    sync_parser.add_argument(
        "--output-dir",
        help="Override the configured absolute output directory for this run",
    )
    sync_parser.add_argument(
        "--class-name",
        help="Only sync the class whose name exactly matches this value",
    )
    sync_parser.add_argument(
        "--workers",
        type=int,
        help="Override worker count for this run",
    )
    sync_parser.add_argument(
        "--force",
        dest="sync",
        action="store_false",
        help="Re-download files even if they already exist locally",
    )
    sync_parser.add_argument(
        "--quiet-download-progress",
        action="store_true",
        help="Hide per-class file download progress bars",
    )
    sync_parser.set_defaults(
        sync=None,
        school_code=None,
        username=None,
        password=None,
        output_dir=None,
        class_name=None,
        workers=None,
        quiet_download_progress=False,
    )

    config_parser = subparsers.add_parser(
        "config",
        help="Inspect or clear saved configuration",
        description="Show or clear saved encrypted configuration.",
    )
    config_subparsers = config_parser.add_subparsers(dest="config_command")

    config_subparsers.add_parser(
        "show",
        help="Show a safe summary of saved configuration",
    )
    config_subparsers.add_parser(
        "clear",
        help="Clear saved configuration files and credentials",
    )

    return parser


def main(argv: Sequence[str] | None = None) -> int:
    """Run the CLI and return a process exit code."""
    parser = build_parser()
    args = parser.parse_args(argv)

    command = args.command or "sync"

    try:
        if command == "configure":
            return run_configure(args)
        if command == "config":
            return run_config(args)
        if command == "sync":
            return run_sync(args)

        parser.print_help()
        return 1
    except KeyboardInterrupt:
        tqdm.write("")
        tqdm.write("Cancelled by user.")
        return 130
    except ConfigError as exc:
        tqdm.write(f"Configuration error: {exc}")
        return 2
    except ManageBacAuthenticationError as exc:
        tqdm.write(f"Authentication error: {exc}")
        return 3
    except Exception as exc:  # noqa: BLE001
        tqdm.write(f"Unexpected error: {exc}")
        return 10


def run_configure(args: argparse.Namespace) -> int:
    """Handle the `configure` command."""
    school_code = args.school_code or _prompt_non_empty(
        "School code",
        hint="The part before .managebac.com",
    )
    username = args.username or _prompt_non_empty("Username / email")
    password = args.password or getpass.getpass("Password: ")
    output_dir_input = args.output_dir or _prompt_optional(
        "Default output directory (full path)",
        default=str(Path.cwd() / "scrapedData"),
    )
    output_dir = _validate_full_output_dir(output_dir_input)

    if args.workers < 1:
        raise ConfigError("workers must be at least 1")

    tqdm.write("")
    tqdm.write("Testing credentials...")
    cookies = login(school_code, username, password)
    logout(school_code, cookies)

    save_full_configuration(
        school_code=school_code,
        username=username,
        password=password,
        output_dir=output_dir,
        workers=args.workers,
        sync=args.sync,
    )

    tqdm.write("")
    tqdm.write("Configuration saved successfully.")
    tqdm.write(f"School code : {school_code}")
    tqdm.write(f"Username    : {username}")
    tqdm.write(f"Output dir  : {output_dir}")
    tqdm.write(f"Workers     : {args.workers}")
    tqdm.write(
        f"Sync mode   : {'enabled (skip existing files)' if args.sync else 'disabled'}"
    )
    return 0


def run_config(args: argparse.Namespace) -> int:
    """Handle the `config` command."""
    subcommand = args.config_command or "show"

    if subcommand == "show":
        summary = get_config_summary()
        tqdm.write("")
        tqdm.write("managebac-fetch configuration")
        tqdm.write("-----------------------------")
        tqdm.write(f"Config dir         : {summary['config_dir']}")
        tqdm.write(f"Config file        : {summary['config_path']}")
        tqdm.write(f"Encryption key     : {summary['key_path']}")
        tqdm.write(f"Credentials saved  : {summary['credentials_saved']}")
        tqdm.write(f"School code        : {summary['school_code'] or '(not set)'}")
        tqdm.write(f"Username           : {summary['username'] or '(not set)'}")
        tqdm.write(f"Output directory   : {summary['output_dir'] or '(not set)'}")
        tqdm.write(f"Workers            : {summary['workers']}")
        tqdm.write(
            "Sync mode          : "
            + ("enabled (skip existing files)" if summary["sync"] else "disabled")
        )
        return 0

    if subcommand == "clear":
        from .config import clear

        clear()
        tqdm.write("Saved configuration cleared.")
        return 0

    return 1


def run_sync(args: argparse.Namespace) -> int:
    """Handle the `sync` command or default invocation."""
    stored_config = load_app_config()

    if not has_saved_credentials():
        raise ConfigError(
            "No saved credentials found. Run 'managebac-fetch configure' first."
        )

    credentials = load_credentials()

    school_code = args.school_code or credentials.school_code
    username = args.username or credentials.username
    password = args.password or credentials.password

    output_dir_input = args.output_dir or stored_config.output_dir
    if not output_dir_input:
        raise ConfigError(
            "No output directory is configured. "
            "Run 'managebac-fetch configure' or pass --output-dir."
        )
    output_dir = _validate_full_output_dir(output_dir_input)

    workers = args.workers if args.workers is not None else stored_config.workers
    if workers < 1:
        raise ConfigError("workers must be at least 1")

    sync_mode = args.sync if args.sync is not None else stored_config.sync

    _print_sync_header(
        output_dir=output_dir,
        workers=workers,
        sync_mode=sync_mode,
        class_name=args.class_name,
        using_saved_credentials=not any(
            [args.school_code, args.username, args.password]
        ),
    )

    tqdm.write("Logging in...")
    cookies = login(school_code, username, password)
    tqdm.write("Logged in.")

    try:
        classes = get_classes(school_code, cookies)
        tqdm.write(f"Classes found: {len(classes)}")
        classes = _filter_classes(classes, args.class_name)

        if not classes:
            if args.class_name:
                tqdm.write(f'No class matched "{args.class_name}".')
            else:
                tqdm.write("No classes were found for this account.")
            return 1

        tqdm.write("")

        overall = DownloadSummary(
            total=0,
            queued=0,
            downloaded=0,
            skipped=0,
            failed=0,
        )

        with Pool(processes=workers, initializer=_init_worker) as job_pool:
            for index, class_info in enumerate(classes, start=1):
                tqdm.write(
                    f"[{index}/{len(classes)}] Scanning class: {class_info.name}"
                )

                files = get_files(school_code, cookies, class_info)

                if not files:
                    tqdm.write("  No files found.")
                    continue

                summary = download_files(
                    files=files,
                    output_dir=Path(output_dir) / get_valid_filename(class_info.name),
                    school_code=school_code,
                    cookies=cookies,
                    sync=sync_mode,
                    job_pool=job_pool,
                    show_progress=not args.quiet_download_progress,
                )

                overall = DownloadSummary(
                    total=overall.total + summary.total,
                    queued=overall.queued + summary.queued,
                    downloaded=overall.downloaded + summary.downloaded,
                    skipped=overall.skipped + summary.skipped,
                    failed=overall.failed + summary.failed,
                )

                _print_class_summary(summary)
                tqdm.write("")

        tqdm.write("Sync complete.")
        tqdm.write("--------------")
        tqdm.write(f"Files found      : {overall.total}")
        tqdm.write(f"Downloaded       : {overall.downloaded}")
        tqdm.write(f"Skipped existing : {overall.skipped}")
        tqdm.write(f"Failed           : {overall.failed}")
        result_label = "Success" if overall.failed == 0 else "Completed with errors"
        tqdm.write(f"Result           : {result_label}")

        return 0 if overall.failed == 0 else 2
    finally:
        if cookies is not None:
            logout(school_code, cookies)


def _init_worker() -> None:
    """Ignore SIGINT in worker processes."""
    signal.signal(signal.SIGINT, signal.SIG_IGN)


def _filter_classes(
    classes: Sequence[ClassInfo],
    class_name: str | None,
) -> list[ClassInfo]:
    """Filter classes by exact name if requested."""
    if class_name is None:
        return list(classes)
    return [class_info for class_info in classes if class_info.name == class_name]


def _print_sync_header(
    *,
    output_dir: str,
    workers: int,
    sync_mode: bool,
    class_name: str | None,
    using_saved_credentials: bool,
) -> None:
    """Print a user-friendly sync header."""
    tqdm.write("")
    tqdm.write("ManageBac file sync")
    tqdm.write("-------------------")
    tqdm.write(f"Output folder : {output_dir}")
    tqdm.write(f"Workers       : {workers}")
    tqdm.write(
        "Sync mode     : "
        + (
            "enabled (skip existing files)"
            if sync_mode
            else "disabled (force re-download)"
        )
    )
    tqdm.write(
        "Credentials   : "
        + (
            "saved configuration"
            if using_saved_credentials
            else "provided via arguments/prompts"
        )
    )
    if class_name:
        tqdm.write(f"Class filter  : {class_name}")
    tqdm.write("")


def _print_class_summary(summary: DownloadSummary) -> None:
    """Print a brief summary of a class download run."""
    if summary.total == 0:
        return

    parts: list[str] = []
    if summary.downloaded > 0:
        parts.append(f"{summary.downloaded} downloaded")
    if summary.skipped > 0:
        parts.append(f"{summary.skipped} skipped")
    if summary.failed > 0:
        parts.append(f"{summary.failed} failed")

    if not parts:
        parts.append("0 files processed")

    tqdm.write(f"  Result: {', '.join(parts)}")


def _prompt_non_empty(prompt_text: str, hint: str | None = None) -> str:
    """Prompt the user until a non-empty string is provided."""
    hint_text = f" ({hint})" if hint else ""
    while True:
        value = input(f"{prompt_text}{hint_text}: ").strip()
        if value:
            return value
        print("This field is required.")


def _prompt_optional(prompt_text: str, default: str) -> str:
    """Prompt the user for an optional value, returning the default if empty."""
    value = input(f"{prompt_text} [{default}]: ").strip()
    return value if value else default


def _validate_full_output_dir(value: str) -> str:
    """Validate and normalize a full output directory path for the current OS."""
    raw_value = value.strip()
    if not raw_value:
        raise ConfigError("output directory must be a non-empty path")

    try:
        normalized_path = normalize_output_dir(raw_value)
        validate_output_dir(normalized_path)
    except ValueError as exc:
        raise ConfigError(str(exc)) from exc

    return normalized_path
