#!/usr/bin/env python3
"""Dynamic Routing Fastest Path Algorithm Selector

This script provides a safe, vendor-neutral way to choose a practical path
algorithm for dynamic network routing optimization.

It does not implement a full routing control plane. Instead, it helps you:
- classify a routing problem,
- compare common algorithm options,
- estimate when Dijkstra, A*, or incremental methods are a better fit,
- generate a concise operational recommendation.

Typical use cases:
- selecting a baseline shortest-path approach,
- reviewing dynamic routing requirements before implementation,
- documenting algorithm choice for engineering review.

The recommendations are heuristic and operational, not authoritative.
Always validate against your own topology, failure model, and policy rules.
"""

from __future__ import annotations

import argparse
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional


class UpdateRate(str, Enum):
    RARE = "rare"
    PERIODIC = "periodic"
    CHURN_HEAVY = "churn-heavy"


class QueryMode(str, Enum):
    SINGLE_SOURCE = "single-source"
    SINGLE_DESTINATION = "single-destination"
    MANY_TO_MANY = "many-to-many"


class WeightProfile(str, Enum):
    NON_NEGATIVE = "non-negative"
    POLICY_LIMITED = "policy-limited"
    MULTI_CRITERIA = "multi-criteria"


@dataclass(frozen=True)
class RoutingProfile:
    query_mode: QueryMode
    update_rate: UpdateRate
    weight_profile: WeightProfile
    topology_size: int
    localized_changes: bool
    heuristic_available: bool
    policy_complexity: str


@dataclass(frozen=True)
class Recommendation:
    algorithm: str
    rationale: List[str]
    cautions: List[str]
    next_steps: List[str]


def parse_topology_size(value: str) -> int:
    try:
        size = int(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError("topology-size must be an integer") from exc
    if size < 1:
        raise argparse.ArgumentTypeError("topology-size must be greater than zero")
    return size


def score_dijkstra(profile: RoutingProfile) -> int:
    score = 0
    if profile.weight_profile == WeightProfile.NON_NEGATIVE:
        score += 4
    if profile.query_mode in {QueryMode.SINGLE_SOURCE, QueryMode.SINGLE_DESTINATION}:
        score += 2
    if profile.update_rate in {UpdateRate.RARE, UpdateRate.PERIODIC}:
        score += 2
    if profile.localized_changes:
        score += 1
    if profile.topology_size <= 5000:
        score += 1
    return score


def score_astar(profile: RoutingProfile) -> int:
    score = 0
    if profile.heuristic_available:
        score += 4
    if profile.query_mode == QueryMode.SINGLE_DESTINATION:
        score += 2
    if profile.weight_profile == WeightProfile.NON_NEGATIVE:
        score += 1
    if profile.policy_complexity == "low":
        score += 1
    return score


def score_incremental(profile: RoutingProfile) -> int:
    score = 0
    if profile.update_rate == UpdateRate.CHURN_HEAVY:
        score += 4
    if profile.localized_changes:
        score += 3
    if profile.query_mode == QueryMode.MANY_TO_MANY:
        score += 1
    if profile.topology_size >= 1000:
        score += 1
    return score


def score_policy_aware(profile: RoutingProfile) -> int:
    score = 0
    if profile.weight_profile in {WeightProfile.POLICY_LIMITED, WeightProfile.MULTI_CRITERIA}:
        score += 4
    if profile.policy_complexity in {"medium", "high"}:
        score += 3
    if profile.query_mode == QueryMode.MANY_TO_MANY:
        score += 1
    if profile.update_rate != UpdateRate.RARE:
        score += 1
    return score


def recommend(profile: RoutingProfile) -> Recommendation:
    candidates = {
        "Dijkstra-style shortest path": score_dijkstra(profile),
        "A* search": score_astar(profile),
        "Incremental shortest-path repair": score_incremental(profile),
        "Policy-aware path selection": score_policy_aware(profile),
    }
    chosen = max(candidates, key=candidates.get)

    rationale: List[str] = []
    cautions: List[str] = []
    next_steps: List[str] = [
        "Validate the recommendation against real topology samples.",
        "Test failure, cost-change, and burst-update scenarios.",
        "Confirm that route oscillation and stale-state handling are addressed.",
    ]

    if chosen == "Dijkstra-style shortest path":
        rationale = [
            "Best fit for non-negative link weights and standard shortest-path routing.",
            "Predictable and easy to validate in link-state workflows.",
            "Good baseline when update rate is moderate and correctness matters most.",
        ]
        cautions = [
            "May require tuning or incremental reuse when churn is high.",
            "Full recomputation can become expensive on large topologies.",
        ]
    elif chosen == "A* search":
        rationale = [
            "A heuristic is available and the search is likely to be target-specific.",
            "Can reduce search space compared with uninformed shortest-path search.",
            "Useful when topology or geography makes the heuristic trustworthy.",
        ]
        cautions = [
            "Heuristic quality determines whether A* is actually faster.",
            "Poor heuristics can erase the benefit and add complexity.",
        ]
    elif chosen == "Incremental shortest-path repair":
        rationale = [
            "Frequent localized updates make recomputing everything wasteful.",
            "Reusing previous results can lower recomputation cost during churn.",
            "Useful when changes affect only part of the graph.",
        ]
        cautions = [
            "Implementation complexity is higher than full recomputation.",
            "Correctness validation is more difficult than with a fresh run.",
        ]
    else:
        rationale = [
            "Policy and multiple constraints are significant parts of the routing problem.",
            "The fastest useful algorithm is the one that returns valid paths under policy.",
            "Practical routing often prioritizes correctness under constraints over pure path length.",
        ]
        cautions = [
            "Make sure policy rules are encoded correctly before optimizing for speed.",
            "Verify that the chosen method does not prefer invalid routes.",
        ]

    return Recommendation(chosen, rationale, cautions, next_steps)


def build_profile(args: argparse.Namespace) -> RoutingProfile:
    return RoutingProfile(
        query_mode=QueryMode(args.query_mode),
        update_rate=UpdateRate(args.update_rate),
        weight_profile=WeightProfile(args.weight_profile),
        topology_size=args.topology_size,
        localized_changes=args.localized_changes,
        heuristic_available=args.heuristic_available,
        policy_complexity=args.policy_complexity,
    )


def format_report(profile: RoutingProfile, recommendation: Recommendation) -> str:
    lines = [
        "Dynamic Routing Fastest Path Recommendation",
        "===========================================",
        f"Query mode: {profile.query_mode.value}",
        f"Update rate: {profile.update_rate.value}",
        f"Weight profile: {profile.weight_profile.value}",
        f"Topology size: {profile.topology_size}",
        f"Localized changes: {'yes' if profile.localized_changes else 'no'}",
        f"Heuristic available: {'yes' if profile.heuristic_available else 'no'}",
        f"Policy complexity: {profile.policy_complexity}",
        "",
        f"Recommended approach: {recommendation.algorithm}",
        "",
        "Why this fits:",
    ]
    for item in recommendation.rationale:
        lines.append(f"- {item}")
    lines.append("")
    lines.append("Cautions:")
    for item in recommendation.cautions:
        lines.append(f"- {item}")
    lines.append("")
    lines.append("Next steps:")
    for item in recommendation.next_steps:
        lines.append(f"- {item}")
    return "\n".join(lines)


def make_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Select a practical path algorithm for dynamic routing optimization."
    )
    parser.add_argument(
        "--query-mode",
        choices=[m.value for m in QueryMode],
        default=QueryMode.SINGLE_DESTINATION.value,
        help="Routing query pattern to optimize for.",
    )
    parser.add_argument(
        "--update-rate",
        choices=[u.value for u in UpdateRate],
        default=UpdateRate.PERIODIC.value,
        help="How often link costs or topology changes occur.",
    )
    parser.add_argument(
        "--weight-profile",
        choices=[w.value for w in WeightProfile],
        default=WeightProfile.NON_NEGATIVE.value,
        help="Nature of routing weights and constraints.",
    )
    parser.add_argument(
        "--topology-size",
        type=parse_topology_size,
        default=1000,
        help="Approximate number of nodes in the routing graph.",
    )
    parser.add_argument(
        "--localized-changes",
        action="store_true",
        help="Set if most updates affect a limited part of the graph.",
    )
    parser.add_argument(
        "--heuristic-available",
        action="store_true",
        help="Set if you have a trustworthy admissible heuristic for A*.",
    )
    parser.add_argument(
        "--policy-complexity",
        choices=["low", "medium", "high"],
        default="medium",
        help="How complex the policy and constraint model is.",
    )
    return parser


def main() -> int:
    parser = make_parser()
    args = parser.parse_args()
    profile = build_profile(args)
    recommendation = recommend(profile)
    print(format_report(profile, recommendation))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())