#!/usr/bin/env python3
"""Valida la struttura minima di un manifest workflow JSON."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any


REQUIRED_FIELDS: dict[str, type] = {
    "id": str,
    "version": str,
    "owner": str,
    "trigger": dict,
    "states": list,
    "steps": list,
    "approvals": list,
    "dependencies": list,
    "rollback": dict,
}
STATE_TYPES = {"action", "decision", "approval", "error", "end"}


def issue(code: str, path: str, message: str) -> dict[str, str]:
    return {"code": code, "path": path, "message": message}


def validate_manifest(data: Any) -> list[dict[str, str]]:
    errors: list[dict[str, str]] = []
    if not isinstance(data, dict):
        return [issue("root_type", "$", "La radice deve essere un oggetto JSON.")]

    for name, expected_type in REQUIRED_FIELDS.items():
        if name not in data:
            errors.append(issue("missing_field", f"$.{name}", "Campo obbligatorio assente."))
        elif not isinstance(data[name], expected_type):
            errors.append(
                issue(
                    "invalid_type",
                    f"$.{name}",
                    f"Tipo atteso: {expected_type.__name__}.",
                )
            )

    for name in ("id", "version", "owner"):
        if isinstance(data.get(name), str) and not data[name].strip():
            errors.append(issue("empty_value", f"$.{name}", "Il valore non può essere vuoto."))

    trigger = data.get("trigger")
    if isinstance(trigger, dict):
        for name in ("type", "start_state"):
            if not isinstance(trigger.get(name), str) or not trigger[name].strip():
                errors.append(issue("invalid_trigger", f"$.trigger.{name}", "Stringa obbligatoria."))

    states = data.get("states")
    state_ids: set[str] = set()
    if isinstance(states, list):
        if not states:
            errors.append(issue("empty_states", "$.states", "Serve almeno uno stato."))
        for index, state in enumerate(states):
            path = f"$.states[{index}]"
            if not isinstance(state, dict):
                errors.append(issue("invalid_state", path, "Lo stato deve essere un oggetto."))
                continue
            state_id = state.get("id")
            if not isinstance(state_id, str) or not state_id.strip():
                errors.append(issue("invalid_state_id", f"{path}.id", "ID di stato obbligatorio."))
            elif state_id in state_ids:
                errors.append(issue("duplicate_state", f"{path}.id", f"ID duplicato: {state_id}."))
            else:
                state_ids.add(state_id)
            state_type = state.get("type")
            if state_type not in STATE_TYPES:
                errors.append(
                    issue("invalid_state_type", f"{path}.type", f"Valori ammessi: {sorted(STATE_TYPES)}.")
                )
            if not isinstance(state.get("next"), list) or not all(
                isinstance(target, str) and target.strip() for target in state.get("next", [])
            ):
                errors.append(issue("invalid_next", f"{path}.next", "Deve essere una lista di ID."))

    if isinstance(trigger, dict) and isinstance(trigger.get("start_state"), str):
        if trigger["start_state"] not in state_ids:
            errors.append(
                issue("unknown_start", "$.trigger.start_state", "Lo stato iniziale non esiste.")
            )

    if isinstance(states, list):
        for index, state in enumerate(states):
            if not isinstance(state, dict) or not isinstance(state.get("next"), list):
                continue
            for target in state["next"]:
                if isinstance(target, str) and target not in state_ids:
                    errors.append(
                        issue(
                            "unknown_target",
                            f"$.states[{index}].next",
                            f"Destinazione inesistente: {target}.",
                        )
                    )
            if state.get("type") == "end" and state["next"]:
                errors.append(
                    issue("end_has_next", f"$.states[{index}].next", "Uno stato finale non deve avere uscite.")
                )

    steps = data.get("steps")
    step_ids: set[str] = set()
    if isinstance(steps, list):
        for index, step in enumerate(steps):
            path = f"$.steps[{index}]"
            if not isinstance(step, dict):
                errors.append(issue("invalid_step", path, "Il passaggio deve essere un oggetto."))
                continue
            for name in ("id", "state", "action"):
                if not isinstance(step.get(name), str) or not step[name].strip():
                    errors.append(issue("invalid_step_field", f"{path}.{name}", "Stringa obbligatoria."))
            if isinstance(step.get("id"), str):
                if step["id"] in step_ids:
                    errors.append(issue("duplicate_step", f"{path}.id", f"ID duplicato: {step['id']}."))
                step_ids.add(step["id"])
            if isinstance(step.get("state"), str) and step["state"] not in state_ids:
                errors.append(issue("unknown_step_state", f"{path}.state", "Lo stato associato non esiste."))

    approvals = data.get("approvals")
    if isinstance(approvals, list):
        for index, approval in enumerate(approvals):
            path = f"$.approvals[{index}]"
            if not isinstance(approval, dict):
                errors.append(issue("invalid_approval", path, "L'approvazione deve essere un oggetto."))
                continue
            if approval.get("state") not in state_ids:
                errors.append(issue("unknown_approval_state", f"{path}.state", "Lo stato non esiste."))
            if not isinstance(approval.get("role"), str) or not approval["role"].strip():
                errors.append(issue("invalid_approval_role", f"{path}.role", "Ruolo obbligatorio."))

    dependencies = data.get("dependencies")
    if isinstance(dependencies, list):
        for index, dependency in enumerate(dependencies):
            path = f"$.dependencies[{index}]"
            if not isinstance(dependency, dict):
                errors.append(issue("invalid_dependency", path, "La dipendenza deve essere un oggetto."))
                continue
            for name in ("name", "type"):
                if not isinstance(dependency.get(name), str) or not dependency[name].strip():
                    errors.append(issue("invalid_dependency_field", f"{path}.{name}", "Stringa obbligatoria."))

    rollback = data.get("rollback")
    if isinstance(rollback, dict):
        if not isinstance(rollback.get("strategy"), str) or not rollback["strategy"].strip():
            errors.append(issue("invalid_rollback", "$.rollback.strategy", "Strategia obbligatoria."))
        if not isinstance(rollback.get("tested"), bool):
            errors.append(issue("invalid_rollback", "$.rollback.tested", "Valore booleano obbligatorio."))

    return errors


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("manifest", type=Path)
    parser.add_argument("--json", action="store_true", dest="as_json", help="Output JSON.")
    args = parser.parse_args()

    try:
        data = json.loads(args.manifest.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        print(f"Impossibile leggere il manifest: {exc}", file=sys.stderr)
        return 2

    errors = validate_manifest(data)
    if args.as_json:
        print(json.dumps({"valid": not errors, "errors": errors}, ensure_ascii=False, indent=2))
    elif errors:
        print(f"Manifest non valido: {len(errors)} problema/i")
        for item in errors:
            print(f"- {item['path']}: {item['message']} [{item['code']}]")
    else:
        print("Manifest valido.")
    return 1 if errors else 0


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