#!/usr/bin/env python3
"""Inspect automation for AI visual checkpoints and install the local helper safely."""

from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any


CONFIG_PATH = Path.home() / ".config" / "carbon" / "config.json"
AI_KEY_PATH = Path.home() / ".config" / "carbon" / "ai-api-key"
KEYCHAIN_SERVICE = "ai.carbon.model-api-key"
KEYCHAIN_ACCOUNT = "local-ai-review"
PROVIDER_ENV = {"openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "gemini": "GEMINI_API_KEY"}
EXCLUDED = {".git", ".carbon", "node_modules", "vendor", "dist", "build", "coverage", "output", ".venv", "venv", "__pycache__"}
TEST_SUFFIXES = {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".java", ".kt", ".cs", ".rb"}
MAX_FILE_BYTES = 1_000_000


def read_json(path: Path, fallback: Any) -> Any:
    try: return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError): return fallback


def keychain_configured() -> bool:
    if sys.platform != "darwin" or os.environ.get("CARBON_CREDENTIAL_STORE") == "file": return False
    try:
        result = subprocess.run(["/usr/bin/security", "find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", KEYCHAIN_SERVICE, "-w"], capture_output=True, check=False)
        return result.returncode == 0 and bool(result.stdout.strip())
    except OSError: return False


def provider_state() -> dict[str, Any]:
    config = read_json(CONFIG_PATH, {})
    requested = str(os.environ.get("CARBON_AI_PROVIDER") or config.get("aiProvider") or "").lower()
    if requested not in PROVIDER_ENV:
        requested = next((provider for provider, variable in PROVIDER_ENV.items() if os.environ.get(variable)), "openai")
    environment = PROVIDER_ENV[requested]
    key_source = environment if os.environ.get(environment) else ("CARBON_AI_API_KEY" if os.environ.get("CARBON_AI_API_KEY") else ("macOS Keychain" if keychain_configured() else (str(AI_KEY_PATH) if AI_KEY_PATH.is_file() else "")))
    model = str(os.environ.get("CARBON_AI_MODEL") or config.get("aiModel") or "").strip()
    return {"provider": requested, "model": model, "keyConfigured": bool(key_source), "configured": bool(key_source and model), "credentialSource": key_source, "credentialEnvironment": environment, "keyValueReturned": False}


def likely_test(path: Path, text: str) -> bool:
    name = path.name.lower()
    parts = {part.lower() for part in path.parts}
    return bool({"test", "tests", "spec", "specs", "e2e", "integration"} & parts or re.search(r"(?:test|spec|e2e)\.", name) or re.search(r"\b(test|it|describe|pytest|webdriver|playwright|cy\.)\b", text, re.I))


def framework(path: Path, text: str) -> str:
    low = text.lower()
    if "@playwright/test" in low or "page.goto(" in low: return "playwright"
    if "cy.visit(" in low or "cypress" in low: return "cypress"
    if "selenium" in low or "webdriver" in low: return "selenium"
    if path.suffix == ".py" and ("pytest" in low or "def test_" in low): return "pytest"
    if "junit" in low or "@test" in low and path.suffix in {".java", ".kt"}: return "junit"
    return "automation"


def candidates(path: Path, text: str, project: Path) -> list[dict[str, Any]]:
    result = []
    patterns = [
        (re.compile(r"\b(?:page\.goto|driver\.get|cy\.visit)\s*\("), "settled-page", "After the initial page reaches its intended visible state"),
        (re.compile(r"\b(?:screenshot|take_screenshot|save_screenshot)\s*\("), "existing-screenshot", "Reuse an existing screenshot checkpoint without adding another capture"),
        (re.compile(r"\b(?:toBeVisible|toHaveURL|toContainText|should\s*\(|assert\s+.+(?:visible|displayed|text|url))"), "verified-state", "After a meaningful user-visible assertion"),
        (re.compile(r"\b(?:checkout|payment|confirmation|success|dashboard|receipt|profile|settings|onboarding|error)\b", re.I), "risk-state", "At a named high-value success, failure, or consequential state"),
    ]
    lines = text.splitlines()
    seen = set()
    for line_number, line in enumerate(lines, 1):
        for pattern, kind, reason in patterns:
            if not pattern.search(line): continue
            key = (line_number, kind)
            if key in seen: continue
            seen.add(key)
            result.append({"path": str(path.relative_to(project)), "line": line_number, "kind": kind, "reason": reason, "anchor": line.strip()[:300], "recommendedDensity": "One check per distinct critical visible state; normally 1–3 per test file."})
            break
    return result[:20]


def inspect_project(root: Path) -> dict[str, Any]:
    tests, insertion_candidates = [], []
    framework_counts: dict[str, int] = {}
    for path in sorted(root.rglob("*")):
        if not path.is_file() or path.suffix.lower() not in TEST_SUFFIXES or any(part in EXCLUDED for part in path.relative_to(root).parts): continue
        try:
            if path.stat().st_size > MAX_FILE_BYTES: continue
            text = path.read_text(encoding="utf-8", errors="replace")
        except OSError: continue
        if not likely_test(path, text): continue
        detected = framework(path, text)
        found = candidates(path, text, root)
        framework_counts[detected] = framework_counts.get(detected, 0) + 1
        tests.append({"path": str(path.relative_to(root)), "framework": detected, "checkpointCandidates": len(found)})
        insertion_candidates.extend(found)
    config = read_json(Path.home() / ".config" / "carbon" / "config.json", {})
    cloud_key = bool(os.environ.get("TESTERS_AI_API_KEY") or (Path.home() / ".config" / "carbon" / "cloud-api-key").is_file())
    if sys.platform == "darwin" and not cloud_key:
        try:
            cloud_key = subprocess.run(["/usr/bin/security", "find-generic-password", "-a", "testers-ai-cloud", "-s", "ai.testers.cloud-api-key", "-w"], capture_output=True, check=False).returncode == 0
        except OSError: pass
    return {"schema": "carbon.ai-upgrade-inspection/v1", "root": str(root), "testFiles": tests, "frameworks": framework_counts, "checkpointCandidates": insertion_candidates[:200], "candidateCount": len(insertion_candidates), "cloudEnrichment": {"configured": cloud_key, "provider": "testers.ai Premium Cloud", "requiresPublicTarget": True, "mutatesCloud": False, "next": "Offer unsaved testers.ai AI check proposals; save or run only after separate approval."}, "localAiScreenshotReview": provider_state(), "recommendation": "Place 1–3 AI visual checks per high-value file after stable visible states; reuse existing screenshots, avoid secrets/PII, and keep deterministic assertions authoritative."}


def command_inspect(args: argparse.Namespace) -> dict[str, Any]:
    root = Path(args.root).expanduser().resolve()
    if not root.is_dir(): raise ValueError("AI Upgrade root must be an existing directory.")
    return inspect_project(root)


def command_install(args: argparse.Namespace) -> dict[str, Any]:
    if not args.confirm: raise ValueError("Helper not installed. Use --confirm only after the user approves adding local AI screenshot checks to this project.")
    root = Path(args.root).expanduser().resolve()
    if not root.is_dir(): raise ValueError("AI Upgrade root must be an existing directory.")
    state = provider_state()
    provider = args.provider or state["provider"]
    model = args.model or state["model"]
    if provider not in PROVIDER_ENV: raise ValueError("Provider must be openai, anthropic, or gemini.")
    if not model: raise ValueError("Configure an exact vision-capable model before installing screenshot checks.")
    target = root / ".carbon" / "upgrade"
    target.mkdir(parents=True, exist_ok=True)
    source = Path(__file__).resolve().parent.parent / "templates" / "carbon_ai_visual_check.py"
    helper = target / "ai-visual-check.py"
    shutil.copy2(source, helper)
    helper.chmod(0o755)
    config = {"schema": "carbon.ai-visual-config/v1", "provider": provider, "model": model, "failOn": args.fail_on, "onError": args.on_error, "privacy": "Selected screenshots are transmitted to the configured provider. No API key is stored in this project."}
    config_path = target / "ai-visual-config.json"
    config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
    return {"schema": "carbon.ai-upgrade-helper-result/v1", "installed": True, "helperPath": str(helper), "configPath": str(config_path), "provider": provider, "model": model, "keyStoredInProject": False, "next": "Use apply_patch to add sparse framework-native screenshot captures and helper calls only at the approved checkpoint candidates, then run the touched tests."}


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description=__doc__)
    commands = root.add_subparsers(dest="command", required=True)
    inspect = commands.add_parser("inspect")
    inspect.add_argument("--root", required=True)
    inspect.set_defaults(handler=command_inspect)
    install = commands.add_parser("install-helper")
    install.add_argument("--root", required=True)
    install.add_argument("--provider", choices=tuple(PROVIDER_ENV))
    install.add_argument("--model")
    install.add_argument("--fail-on", choices=("never", "critical", "high", "medium", "any"), default="critical")
    install.add_argument("--on-error", choices=("warn", "fail"), default="warn")
    install.add_argument("--confirm", action="store_true")
    install.set_defaults(handler=command_install)
    return root


def main() -> int:
    args = parser().parse_args()
    try: print(json.dumps(args.handler(args), indent=2)); return 0
    except (ValueError, OSError) as error: print(str(error), file=sys.stderr); return 2


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