from __future__ import annotations

import argparse
import json
import shlex
import socket
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any

from dockervault.classifier import classify_compose


def check_path_exists(path: str) -> bool:
    return Path(path).exists()


def create_missing_paths(paths: list[str]) -> list[str]:
    created: list[str] = []
    for path in sorted(set(paths)):
        p = Path(path)
        if not p.exists():
            p.mkdir(parents=True, exist_ok=True)
            created.append(str(p))
    return created


def build_mkdir_suggestion(paths: list[str]) -> str:
    unique_paths = sorted(set(paths))
    lines = ["mkdir -p \\"]
    for index, path in enumerate(unique_paths):
        suffix = " \\" if index < len(unique_paths) - 1 else ""
        lines.append(f"  {path}{suffix}")
    return "\n".join(lines)


def render_borg_archive(template: str, project: str, compose_path: Path) -> str:
    now = datetime.now()
    hostname = socket.gethostname()
    compose_stem = compose_path.stem

    return template.format(
        hostname=hostname,
        project=project,
        compose_stem=compose_stem,
        now=now,
    )


def build_borg_command(repo: str, archive_name: str, include_paths: list[str]) -> str:
    lines = [
        "borg create --stats --progress \\",
        f"  {repo}::{archive_name} \\",
    ]

    for index, path in enumerate(include_paths):
        suffix = " \\" if index < len(include_paths) - 1 else ""
        lines.append(f"  {path}{suffix}")

    return "\n".join(lines)


def build_borg_argv(repo: str, archive_name: str, include_paths: list[str]) -> list[str]:
    return [
        "borg",
        "create",
        "--stats",
        "--progress",
        f"{repo}::{archive_name}",
        *include_paths,
    ]


def find_missing_paths(
    plan: dict[str, Any],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    missing_include = [
        item for item in plan.get("include", [])
        if not check_path_exists(item["source"])
    ]

    missing_review = [
        item for item in plan.get("review", [])
        if not check_path_exists(item["source"])
    ]

    return missing_include, missing_review


def print_human_summary(compose_file: Path, project_root: Path, plan: dict[str, Any]) -> None:
    print("DockerVault Backup Plan")
    print("=======================")
    print(f"Compose file: {compose_file.resolve()}")
    print(f"Project root: {project_root.resolve()}")
    print()

    for section in ["include", "review", "skip"]:
        print(f"{section.upper()} PATHS:")
        items = plan.get(section, [])
        if items:
            for item in items:
                exists = check_path_exists(item["source"])
                status = "✔ exists" if exists else "❌ missing"
                print(
                    f"  - {item['source']} "
                    f"[{item['priority']}] {status} "
                    f"service={item['service']} target={item['target']}"
                )
        else:
            print("  - (none)")
        print()


def print_missing_paths_report(
    missing_include: list[dict[str, Any]],
    missing_review: list[dict[str, Any]],
) -> None:
    all_missing = missing_include + missing_review
    if not all_missing:
        return

    print("WARNING: Missing paths detected:")
    for item in all_missing:
        bucket = "include" if item in missing_include else "review"
        print(f"  - {item['source']} (service={item['service']}, bucket={bucket})")
    print()


def print_created_paths(created_paths: list[str]) -> None:
    if not created_paths:
        return

    print("Created missing paths:")
    for path in created_paths:
        print(f"  - {path}")
    print()


def plan_to_json_dict(
    compose_file: Path,
    project_root: Path,
    plan: dict[str, Any],
    borg_repo: str | None = None,
    borg_archive: str | None = None,
    borg_command: str | None = None,
    missing_include: list[dict[str, Any]] | None = None,
    missing_review: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
    return {
        "compose_file": str(compose_file.resolve()),
        "project_root": str(project_root.resolve()),
        "include": plan.get("include", []),
        "review": plan.get("review", []),
        "skip": plan.get("skip", []),
        "missing": {
            "include": missing_include or [],
            "review": missing_review or [],
        },
        "borg": {
            "repo": borg_repo,
            "archive": borg_archive,
            "command": borg_command,
        }
        if borg_repo or borg_archive or borg_command
        else None,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="DockerVault - intelligent Docker backup discovery"
    )

    parser.add_argument(
        "compose_file",
        nargs="?",
        default="docker-compose.yml",
        help="Path to docker-compose.yml",
    )

    parser.add_argument(
        "--json",
        action="store_true",
        help="Print plan as JSON",
    )

    parser.add_argument(
        "--borg",
        action="store_true",
        help="Show suggested borg create command",
    )

    parser.add_argument(
        "--run-borg",
        action="store_true",
        help="Execute borg create",
    )

    parser.add_argument(
        "--borg-repo",
        default="/backup-repo",
        help="Borg repository path or URI (default: /backup-repo)",
    )

    parser.add_argument(
        "--borg-archive",
        default="{hostname}-{now:%Y-%m-%d_%H-%M}",
        help=(
            "Archive naming template. Supported fields: "
            "{hostname}, {project}, {compose_stem}, {now:...}"
        ),
    )

    parser.add_argument(
        "--fail-on-missing",
        action="store_true",
        help="Exit with status 2 if include/review paths are missing",
    )

    parser.add_argument(
        "--apply-mkdir",
        action="store_true",
        help="Create missing include/review paths",
    )

    args = parser.parse_args()

    compose_file = Path(args.compose_file).resolve()
    if not compose_file.exists():
        raise SystemExit(f"Compose file not found: {compose_file}")

    project_root = compose_file.parent
    project_name = project_root.name or compose_file.stem

    plan = classify_compose(compose_file)

    missing_include, missing_review = find_missing_paths(plan)
    all_missing = missing_include + missing_review

    created_paths: list[str] = []
    if args.apply_mkdir and all_missing:
        created_paths = create_missing_paths([item["source"] for item in all_missing])
        missing_include, missing_review = find_missing_paths(plan)
        all_missing = missing_include + missing_review

    borg_command: str | None = None
    borg_argv: list[str] | None = None
    archive_name: str | None = None

    if args.borg or args.run_borg:
        include_paths = [item["source"] for item in plan.get("include", [])]

        try:
            archive_name = render_borg_archive(
                args.borg_archive,
                project_name,
                compose_file,
            )
        except KeyError as exc:
            raise SystemExit(
                f"Invalid borg archive template field: {exc}. "
                "Allowed: hostname, project, compose_stem, now"
            ) from exc

        borg_command = build_borg_command(
            repo=args.borg_repo,
            archive_name=archive_name,
            include_paths=include_paths,
        )

        borg_argv = build_borg_argv(
            repo=args.borg_repo,
            archive_name=archive_name,
            include_paths=include_paths,
        )

    if args.json:
        print(
            json.dumps(
                plan_to_json_dict(
                    compose_file=compose_file,
                    project_root=project_root,
                    plan=plan,
                    borg_repo=args.borg_repo if (args.borg or args.run_borg) else None,
                    borg_archive=archive_name,
                    borg_command=borg_command,
                    missing_include=missing_include,
                    missing_review=missing_review,
                ),
                indent=2,
            )
        )
        if args.fail_on_missing and all_missing:
            sys.exit(2)
        return

    print_human_summary(compose_file, project_root, plan)
    print_missing_paths_report(missing_include, missing_review)
    print_created_paths(created_paths)

    if all_missing and not args.apply_mkdir:
        print("Suggested fix:")
        print(build_mkdir_suggestion([item["source"] for item in all_missing]))
        print()

    if borg_command:
        print("Suggested borg command:")
        print(borg_command)
        print()

    if args.fail_on_missing and all_missing:
        print("ERROR: Failing because include/review paths are missing.")
        sys.exit(2)

    if args.run_borg:
        if borg_argv is None:
            raise SystemExit("Internal error: borg command was not prepared")

        print("Running borg create...")
        print(" ".join(shlex.quote(part) for part in borg_argv))
        print()

        try:
            completed = subprocess.run(borg_argv, check=False)
        except FileNotFoundError as exc:
            raise SystemExit("borg executable not found in PATH") from exc

        if completed.returncode != 0:
            raise SystemExit(completed.returncode)


if __name__ == "__main__":
    main()
