#!/usr/bin/env python3
"""python_production_readiness_review.py

A vendor-neutral Python 3 script for running a production readiness review
for Python applications.

Features:
- Interactive or non-interactive checklist execution
- Section-by-section review items aligned to production readiness concerns
- Evidence and notes capture
- JSON export for audit trails or follow-up
- No external dependencies
- No secrets, credentials, or environment-specific endpoints

Usage examples:
  python python_production_readiness_review.py
  python python_production_readiness_review.py --export review.json
  python python_production_readiness_review.py --answers answers.json --export review.json
  python python_production_readiness_review.py --skip-interactive --answers answers.json

Answers file format (JSON):
{
  "reviewer": "Alex",
  "project": "my-service",
  "items": {
    "1.1": {"status": "yes", "evidence": "Version matrix in docs/version-support.md", "notes": ""},
    "1.2": {"status": "no", "evidence": "", "notes": "Need container baseline confirmation"}
  }
}
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import dataclass, asdict, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Tuple


STATUSES = {"yes", "no", "partial", "na"}
PASS_STATUSES = {"yes", "na"}
FAIL_STATUSES = {"no", "partial"}


@dataclass
class ChecklistItem:
    id: str
    phase: str
    prompt: str
    acceptance_hint: str


@dataclass
class ReviewResponse:
    status: str
    evidence: str = ""
    notes: str = ""


@dataclass
class ReviewResult:
    generated_at: str
    reviewer: str
    project: str
    summary: Dict[str, int]
    results: Dict[str, Dict[str, str]] = field(default_factory=dict)


CHECKLIST: List[ChecklistItem] = [
    ChecklistItem("1.1", "Phase 1: Scope and runtime baseline", "Confirm the Python version(s) supported and document the minimum and maximum tested versions.", "Supported versions are documented."),
    ChecklistItem("1.2", "Phase 1: Scope and runtime baseline", "Review the production OS, container image, or server baseline and document runtime constraints.", "Runtime baseline is documented."),
    ChecklistItem("1.3", "Phase 1: Scope and runtime baseline", "Validate whether the application is a web service, batch job, CLI tool, library, or scheduled task.", "Use case is identified and controls match it."),
    ChecklistItem("1.4", "Phase 1: Scope and runtime baseline", "Assign ownership for code, deployment, secrets, and runtime operations.", "Ownership is explicit."),
    ChecklistItem("1.5", "Phase 1: Scope and runtime baseline", "Document external dependencies such as databases, queues, storage, identity providers, and third-party APIs.", "Dependencies are documented."),
    ChecklistItem("1.6", "Phase 1: Scope and runtime baseline", "Review whether version-specific behavior, optional C extensions, or platform-specific packages affect supportability.", "Supportability risks are understood."),
    ChecklistItem("1.7", "Phase 1: Scope and runtime baseline", "Confirm the release scope: what changed, what did not change, and what is out of scope.", "Scope is explicit."),

    ChecklistItem("2.1", "Phase 2: Source control and change integrity", "Confirm all production code changes are tracked in version control.", "All changes are in version control."),
    ChecklistItem("2.2", "Phase 2: Source control and change integrity", "Review branch protection, required approvals, and merge rules for the mainline branch.", "Merge rules are defined."),
    ChecklistItem("2.3", "Phase 2: Source control and change integrity", "Validate that release tags or commit identifiers map to a build artifact.", "Artifact traceability exists."),
    ChecklistItem("2.4", "Phase 2: Source control and change integrity", "Document how hotfixes, emergency changes, and rollback commits are approved.", "Exception handling is documented."),
    ChecklistItem("2.5", "Phase 2: Source control and change integrity", "Assign responsibility for approving release merges and release notes.", "Approval ownership is clear."),
    ChecklistItem("2.6", "Phase 2: Source control and change integrity", "Test that the build pipeline pulls from the expected repository and branch.", "Pipeline source is verified."),
    ChecklistItem("2.7", "Phase 2: Source control and change integrity", "Review whether generated files, lock files, and vendored code are intentionally committed and kept in sync.", "Repository content policy is understood."),

    ChecklistItem("3.1", "Phase 3: Dependencies and packaging", "Confirm dependency management uses a repeatable method such as a lock file, constraints file, or pinned build manifest.", "Dependencies are pinned or controlled."),
    ChecklistItem("3.2", "Phase 3: Dependencies and packaging", "Validate that installation is reproducible from a clean environment.", "Clean install is reproducible."),
    ChecklistItem("3.3", "Phase 3: Dependencies and packaging", "Review whether build-time and runtime dependencies are separated where appropriate.", "Dependency separation is intentional."),
    ChecklistItem("3.4", "Phase 3: Dependencies and packaging", "Document any packages that require native compilation, external libraries, or OS-level headers.", "Native requirements are documented."),
    ChecklistItem("3.5", "Phase 3: Dependencies and packaging", "Confirm that private package indexes, mirrors, or artifact repositories are reachable in the deployment environment.", "Package source access is validated."),
    ChecklistItem("3.6", "Phase 3: Dependencies and packaging", "Test that dependency installation succeeds without relying on a developer workstation state.", "Install is independent of local state."),
    ChecklistItem("3.7", "Phase 3: Dependencies and packaging", "Review whether optional dependencies are enabled only when explicitly required.", "Optional features are controlled."),
    ChecklistItem("3.8", "Phase 3: Dependencies and packaging", "Validate that package metadata, entry points, and import paths resolve correctly.", "Packaging metadata resolves."),

    ChecklistItem("4.1", "Phase 4: Code quality and test coverage", "Confirm unit tests cover critical logic paths and error handling branches.", "Critical paths are tested."),
    ChecklistItem("4.2", "Phase 4: Code quality and test coverage", "Review integration tests for database, queue, file system, and API interactions that matter in production.", "Integration behavior is tested."),
    ChecklistItem("4.3", "Phase 4: Code quality and test coverage", "Validate that test data is realistic enough without exposing sensitive information.", "Test data is appropriate."),
    ChecklistItem("4.4", "Phase 4: Code quality and test coverage", "Test that the application fails safely when upstream services are unavailable or return invalid data.", "Failure handling is verified."),
    ChecklistItem("4.5", "Phase 4: Code quality and test coverage", "Document any known gaps in coverage and assign an owner for remediation.", "Coverage gaps are tracked."),
    ChecklistItem("4.6", "Phase 4: Code quality and test coverage", "Review whether static analysis, type checking, or linting gates are required for merges.", "Quality gates are defined."),
    ChecklistItem("4.7", "Phase 4: Code quality and test coverage", "Confirm that tests run in automation and are not dependent on manual setup.", "Tests are automated."),
    ChecklistItem("4.8", "Phase 4: Code quality and test coverage", "Validate that smoke tests cover the main deployment path and startup sequence.", "Smoke tests exist."),

    ChecklistItem("5.1", "Phase 5: Security and secrets handling", "Confirm secrets are not stored in source files, notebooks, fixtures, or build artifacts.", "Secrets stay out of source control."),
    ChecklistItem("5.2", "Phase 5: Security and secrets handling", "Review how secrets are injected at runtime and whether rotation is supported.", "Secret injection is controlled."),
    ChecklistItem("5.3", "Phase 5: Security and secrets handling", "Validate that input validation exists for external data, file names, paths, and query parameters.", "Input validation exists."),
    ChecklistItem("5.4", "Phase 5: Security and secrets handling", "Test that command execution, file writes, and deserialization paths are restricted to trusted inputs.", "Unsafe operations are restricted."),
    ChecklistItem("5.5", "Phase 5: Security and secrets handling", "Document dependency vulnerability scanning and how findings are triaged.", "Vulnerability handling is defined."),
    ChecklistItem("5.6", "Phase 5: Security and secrets handling", "Confirm logging redacts tokens, passwords, personal data, and other sensitive fields.", "Logs are redacted."),
    ChecklistItem("5.7", "Phase 5: Security and secrets handling", "Review whether environment-specific security settings are required and documented.", "Security settings are documented."),
    ChecklistItem("5.8", "Phase 5: Security and secrets handling", "Validate that least-privilege permissions are used for the application identity and file system access.", "Least privilege is applied."),
]


def normalize_status(value: str) -> str:
    status = value.strip().lower()
    if status not in STATUSES:
        raise ValueError(f"Invalid status: {value!r}. Use one of: {', '.join(sorted(STATUSES))}")
    return status


def load_answers(path: Path) -> Tuple[str, str, Dict[str, ReviewResponse]]:
    data = json.loads(path.read_text(encoding="utf-8"))
    reviewer = str(data.get("reviewer", "")).strip()
    project = str(data.get("project", "")).strip()
    items = data.get("items", {})
    responses: Dict[str, ReviewResponse] = {}
    if not isinstance(items, dict):
        raise ValueError("'items' must be an object keyed by checklist item id")
    for item_id, item_data in items.items():
        if not isinstance(item_data, dict):
            raise ValueError(f"Item {item_id} must be an object")
        responses[str(item_id)] = ReviewResponse(
            status=normalize_status(str(item_data.get("status", ""))),
            evidence=str(item_data.get("evidence", "")).strip(),
            notes=str(item_data.get("notes", "")).strip(),
        )
    return reviewer, project, responses


def prompt_response(item: ChecklistItem) -> ReviewResponse:
    print(f"\n{item.id} | {item.phase}")
    print(item.prompt)
    print("Status: yes / no / partial / na")
    while True:
        try:
            status = normalize_status(input("> "))
            break
        except ValueError as exc:
            print(exc)
    evidence = input("Evidence (optional): ").strip()
    notes = input("Notes (optional): ").strip()
    return ReviewResponse(status=status, evidence=evidence, notes=notes)


def summarize(responses: Dict[str, ReviewResponse]) -> Dict[str, int]:
    summary = {"yes": 0, "no": 0, "partial": 0, "na": 0, "passed": 0, "failed": 0, "total": len(CHECKLIST)}
    for resp in responses.values():
        summary[resp.status] += 1
        if resp.status in PASS_STATUSES:
            summary["passed"] += 1
        elif resp.status in FAIL_STATUSES:
            summary["failed"] += 1
    return summary


def build_result(reviewer: str, project: str, responses: Dict[str, ReviewResponse]) -> ReviewResult:
    result_map = {item_id: asdict(resp) for item_id, resp in responses.items()}
    return ReviewResult(
        generated_at=datetime.now(timezone.utc).isoformat(),
        reviewer=reviewer,
        project=project,
        summary=summarize(responses),
        results=result_map,
    )


def main() -> int:
    parser = argparse.ArgumentParser(description="Run a Python production readiness review.")
    parser.add_argument("--project", default="", help="Project or service name")
    parser.add_argument("--reviewer", default="", help="Reviewer name")
    parser.add_argument("--answers", type=Path, help="Path to a JSON answers file")
    parser.add_argument("--export", type=Path, help="Write completed review results to JSON")
    parser.add_argument("--skip-interactive", action="store_true", help="Do not prompt interactively; require --answers")
    args = parser.parse_args()

    reviewer = args.reviewer.strip()
    project = args.project.strip()
    responses: Dict[str, ReviewResponse] = {}

    if args.answers:
        file_reviewer, file_project, file_responses = load_answers(args.answers)
        reviewer = reviewer or file_reviewer
        project = project or file_project
        responses.update(file_responses)

    if not args.skip_interactive:
        print("Python Production Readiness Review")
        if not reviewer:
            reviewer = input("Reviewer name: ").strip()
        if not project:
            project = input("Project/service name: ").strip()
        for item in CHECKLIST:
            if item.id in responses:
                continue
            responses[item.id] = prompt_response(item)
    else:
        missing = [item.id for item in CHECKLIST if item.id not in responses]
        if missing:
            print("Missing answers for: " + ", ".join(missing), file=sys.stderr)
            return 2

    result = build_result(reviewer, project, responses)

    print("\nReview Summary")
    print(f"Project: {result.project or '(unspecified)'}")
    print(f"Reviewer: {result.reviewer or '(unspecified)'}")
    print(f"Total items: {result.summary['total']}")
    print(f"Passed: {result.summary['passed']}")
    print(f"Failed: {result.summary['failed']}")
    print(f"Yes: {result.summary['yes']} | No: {result.summary['no']} | Partial: {result.summary['partial']} | NA: {result.summary['na']}")

    if result.summary["failed"] > 0:
        print("\nStatus: NOT READY FOR PRODUCTION")
    else:
        print("\nStatus: READY FOR PRODUCTION REVIEW CONTINGENT ON EVIDENCE QUALITY")

    if args.export:
        args.export.write_text(json.dumps(asdict(result), indent=2), encoding="utf-8")
        print(f"\nExported results to: {args.export}")

    return 0


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