#!/usr/bin/env python3
"""JWT validation readiness checker for C# APIs.

This script provides a lightweight, vendor-neutral way to review whether a JWT-based
C# API design is ready for production use. It does not validate real tokens or call
external services. Instead, it evaluates a checklist of implementation choices that
matter for authentication, authorization, and operational safety.

Typical use cases:
- Review a planned JWT setup before implementation
- Audit a C# API design for common auth mistakes
- Create a repeatable readiness report for teams

Examples:
    python jwt_readiness_checker.py --interactive
    python jwt_readiness_checker.py --issuer https://issuer.example --audience api://orders \
        --access-token-lifetime-minutes 15 --require-tenant-check --key-rotation-planned

Exit codes:
    0 = ready or informational output only
    1 = warnings or failed checks were found
"""

from __future__ import annotations

import argparse
import sys
from dataclasses import dataclass, field
from typing import List, Tuple


@dataclass
class Assessment:
    passed: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    failed: List[str] = field(default_factory=list)

    def add(self, condition: bool, pass_msg: str, fail_msg: str, warn: bool = False) -> None:
        if condition:
            self.passed.append(pass_msg)
        elif warn:
            self.warnings.append(fail_msg)
        else:
            self.failed.append(fail_msg)

    def score(self) -> Tuple[int, int]:
        total = len(self.passed) + len(self.warnings) + len(self.failed)
        earned = len(self.passed)
        return earned, total


def positive_int(value: str) -> int:
    try:
        ivalue = int(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError(f"expected an integer, got {value!r}") from exc
    if ivalue < 0:
        raise argparse.ArgumentTypeError("value must be zero or greater")
    return ivalue


def non_empty(value: str) -> str:
    if not value.strip():
        raise argparse.ArgumentTypeError("value must not be empty")
    return value.strip()


def yesno(prompt: str) -> bool:
    while True:
        raw = input(f"{prompt} [y/n]: ").strip().lower()
        if raw in {"y", "yes"}:
            return True
        if raw in {"n", "no"}:
            return False
        print("Please answer y or n.")


def interactive_args(namespace: argparse.Namespace) -> argparse.Namespace:
    print("Interactive JWT readiness review\n")
    if not namespace.issuer:
        namespace.issuer = input("Expected issuer: ").strip()
    if not namespace.audience:
        namespace.audience = input("Expected audience: ").strip()
    if namespace.access_token_lifetime_minutes is None:
        raw = input("Access token lifetime in minutes (recommended: 5-15): ").strip()
        namespace.access_token_lifetime_minutes = int(raw) if raw else 0
    if namespace.require_tenant_check is None:
        namespace.require_tenant_check = yesno("Does the API enforce tenant/resource ownership checks?")
    if namespace.key_rotation_planned is None:
        namespace.key_rotation_planned = yesno("Is key rotation planned and tested?")
    if namespace.policy_based_auth is None:
        namespace.policy_based_auth = yesno("Are authorization rules centralized in policies?")
    if namespace.explicit_issuer_check is None:
        namespace.explicit_issuer_check = yesno("Is issuer validation explicit and exact?")
    if namespace.explicit_audience_check is None:
        namespace.explicit_audience_check = yesno("Is audience validation explicit and exact?")
    if namespace.short_lifetime_mandated is None:
        namespace.short_lifetime_mandated = yesno("Are short-lived access tokens required?")
    return namespace


def assess(namespace: argparse.Namespace) -> Assessment:
    report = Assessment()

    issuer_ok = bool(namespace.issuer)
    audience_ok = bool(namespace.audience)
    lifetime_ok = 1 <= namespace.access_token_lifetime_minutes <= 15

    report.add(
        issuer_ok,
        "Issuer is defined.",
        "Issuer is missing; tokens may be accepted from an untrusted source.",
    )
    report.add(
        audience_ok,
        "Audience is defined.",
        "Audience is missing; tokens may be valid for the wrong API.",
    )
    report.add(
        namespace.explicit_issuer_check,
        "Issuer validation is explicit.",
        "Issuer validation should be explicit and exact.",
    )
    report.add(
        namespace.explicit_audience_check,
        "Audience validation is explicit.",
        "Audience validation should be explicit and exact.",
    )
    report.add(
        lifetime_ok,
        "Access token lifetime is within a short-lived range (1-15 minutes).",
        "Access token lifetime is long or undefined; consider shorter expiry for production.",
        warn=True,
    )
    report.add(
        namespace.short_lifetime_mandated,
        "Short-lived access tokens are part of the design.",
        "Short-lived tokens are not mandated; this may increase revocation risk.",
        warn=True,
    )
    report.add(
        namespace.require_tenant_check,
        "Tenant or resource ownership checks are enforced.",
        "Tenant/resource checks are missing; authenticated users may access data outside their boundary.",
    )
    report.add(
        namespace.policy_based_auth,
        "Authorization is policy-based and centralized.",
        "Authorization should be centralized in policies rather than scattered across endpoints.",
    )
    report.add(
        namespace.key_rotation_planned,
        "Key rotation is planned and tested.",
        "Key rotation is not planned; signing-key changes may cause outages or unsafe fallback behavior.",
    )

    return report


def print_report(report: Assessment) -> None:
    earned, total = report.score()
    print("\nJWT readiness report")
    print("-" * 24)
    print(f"Passed:   {len(report.passed)}")
    print(f"Warnings: {len(report.warnings)}")
    print(f"Failed:   {len(report.failed)}")
    print(f"Score:    {earned}/{total}")

    if report.passed:
        print("\nPassed checks:")
        for item in report.passed:
            print(f"  [OK] {item}")

    if report.warnings:
        print("\nWarnings:")
        for item in report.warnings:
            print(f"  [WARN] {item}")

    if report.failed:
        print("\nFailed checks:")
        for item in report.failed:
            print(f"  [FAIL] {item}")

    if report.failed:
        print("\nResult: Not ready for production use.")
    elif report.warnings:
        print("\nResult: Potentially ready, but review warnings before production.")
    else:
        print("\nResult: Ready from a basic JWT design perspective.")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Review JWT authentication and authorization readiness for a C# API.",
    )
    parser.add_argument("--issuer", type=non_empty, help="Expected token issuer.")
    parser.add_argument("--audience", type=non_empty, help="Expected token audience.")
    parser.add_argument(
        "--access-token-lifetime-minutes",
        type=positive_int,
        default=0,
        help="Access token lifetime in minutes (recommended: 5-15). Use 0 if unknown.",
    )
    parser.add_argument(
        "--require-tenant-check",
        action=argparse.BooleanOptionalAction,
        default=None,
        help="Require tenant or resource ownership checks.",
    )
    parser.add_argument(
        "--key-rotation-planned",
        action=argparse.BooleanOptionalAction,
        default=None,
        help="Indicate whether signing key rotation is planned and tested.",
    )
    parser.add_argument(
        "--policy-based-auth",
        action=argparse.BooleanOptionalAction,
        default=None,
        help="Indicate whether authorization rules are centralized in policies.",
    )
    parser.add_argument(
        "--explicit-issuer-check",
        action=argparse.BooleanOptionalAction,
        default=None,
        help="Indicate whether issuer validation is explicit and exact.",
    )
    parser.add_argument(
        "--explicit-audience-check",
        action=argparse.BooleanOptionalAction,
        default=None,
        help="Indicate whether audience validation is explicit and exact.",
    )
    parser.add_argument(
        "--short-lifetime-mandated",
        action=argparse.BooleanOptionalAction,
        default=None,
        help="Indicate whether short-lived access tokens are part of the design.",
    )
    parser.add_argument(
        "--interactive",
        action="store_true",
        help="Prompt for missing values interactively.",
    )
    return parser


def main(argv: List[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    if args.interactive:
        args = interactive_args(args)

    report = assess(args)
    print_report(report)

    return 1 if report.failed or report.warnings else 0


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