"""CLI wrapper for SHACL validation."""

from __future__ import annotations

from pathlib import Path
from typing import Iterable

import typer

from wordlift_sdk.validation import validate_file
from worai.errors import UsageError
from typer.core import TyperGroup
import click
from rdflib import BNode, Graph, Namespace, URIRef, RDF
from rdflib.collection import Collection

_SH = Namespace("http://www.w3.org/ns/shacl#")
_SCHEMA = "http://schema.org/"


def _shorten_term(term: URIRef) -> str:
    value = str(term)
    if value.startswith(_SCHEMA):
        return value[len(_SCHEMA) :]
    if "#" in value:
        return value.rsplit("#", 1)[-1]
    if "/" in value:
        return value.rsplit("/", 1)[-1]
    return value


def _path_to_str(graph: Graph, path: URIRef | BNode | None) -> str:
    if path is None:
        return ""
    if isinstance(path, BNode):
        items = [item for item in Collection(graph, path)]
        if items:
            return ".".join(_shorten_term(item) if isinstance(item, URIRef) else str(item) for item in items)
        return str(path)
    if isinstance(path, URIRef):
        return _shorten_term(path)
    return str(path)


def _describe_path(graph: Graph, path: URIRef | BNode | None) -> str:
    if path is None:
        return ""
    if isinstance(path, BNode):
        inv = graph.value(path, _SH.inversePath)
        if inv:
            return f"inverse { _shorten_term(inv) }"
        items = [item for item in Collection(graph, path)]
        if items:
            return ".".join(_shorten_term(item) if isinstance(item, URIRef) else str(item) for item in items)
    if isinstance(path, URIRef):
        return _shorten_term(path)
    return str(path)


def _or_shape_hint(graph: Graph, shape: URIRef | BNode | None) -> str | None:
    if shape is None:
        return None
    or_list = graph.value(shape, _SH["or"])
    if not or_list:
        return None
    alts = []
    for alt in Collection(graph, or_list):
        prop = graph.value(alt, _SH.property)
        if prop is None:
            continue
        path = graph.value(prop, _SH.path)
        if path is None:
            continue
        desc = _describe_path(graph, path)
        if desc:
            alts.append(desc)
    if not alts:
        return None
    if len(alts) == 1:
        return f"Provide: {alts[0]}."
    return f"Provide one of: {', '.join(alts)}."


def _format_pretty_lines(
    report_graph: Graph,
    data_graph: Graph | None,
    shape_source_map: dict[URIRef | BNode, str] | None,
) -> tuple[str, list[tuple[str, str]]]:
    results = []
    for result in report_graph.subjects(_SH.resultSeverity, None):
        severity = report_graph.value(result, _SH.resultSeverity)
        focus = report_graph.value(result, _SH.focusNode)
        path = report_graph.value(result, _SH.resultPath)
        message = report_graph.value(result, _SH.resultMessage)
        source_shape = report_graph.value(result, _SH.sourceShape)
        value_node = report_graph.value(result, _SH.value)
        results.append(
            {
                "severity": severity,
                "focus": focus,
                "path": path,
                "message": str(message) if message else "",
                "source_shape": source_shape,
                "value_node": value_node,
            }
        )

    if not results:
        return "Status: OK (0 errors, 0 warnings, 0 infos)", []

    def severity_label(sev: URIRef | None) -> str:
        if sev == _SH.Violation:
            return "ERROR"
        if sev == _SH.Warning:
            return "WARN"
        if sev == _SH.Info:
            return "INFO"
        return "WARN"

    errors = sum(1 for r in results if r["severity"] == _SH.Violation)
    warnings = sum(1 for r in results if r["severity"] == _SH.Warning)
    infos = sum(1 for r in results if r["severity"] == _SH.Info)

    status = "ERROR" if errors else "WARNING" if warnings else "INFO" if infos else "OK"
    lines: list[tuple[str, str]] = []
    header = f"Status: {status} ({errors} errors, {warnings} warnings, {infos} infos)"

    by_focus: dict[str, list[dict]] = {}
    for r in results:
        focus = str(r["focus"]) if r["focus"] is not None else "(unknown)"
        by_focus.setdefault(focus, []).append(r)

    for focus, items in by_focus.items():
        focus_types: list[str] = []
        if data_graph is not None and focus != "(unknown)":
            for t in data_graph.objects(URIRef(focus), RDF.type):
                focus_types.append(_shorten_term(t))
        if focus_types:
            lines.append(("text", f"Entity: {focus} (types: {', '.join(sorted(set(focus_types)))})"))
        else:
            lines.append(("text", f"Entity: {focus}"))
        for item in items:
            label = severity_label(item["severity"])
            path = _path_to_str(report_graph, item["path"])
            msg = item["message"]
            value_node = item.get("value_node")
            value_str = f" Value: {value_node}" if value_node is not None else ""
            focus = item.get("focus")
            focus_str = f" Focus: {focus}" if focus is not None else ""
            shape = item.get("source_shape")
            shape_str = ""
            if shape is not None:
                mapped = shape_source_map.get(shape) if shape_source_map else None
                if mapped:
                    shape_str = f" [shape: {mapped}]"
                elif isinstance(shape, URIRef):
                    shape_str = f" [shape: {_shorten_term(shape)}]"
                else:
                    shape_str = " [shape: (anonymous)]"
            google_required_suffix = ""
            if label == "ERROR" and shape is not None:
                shape_text = str(shape)
                if "google_" in shape_text or "/google/" in shape_text:
                    google_required_suffix = " Required by Google."
            if msg.startswith("Less than 1 values") and path:
                prefix = "Missing recommended property" if label == "WARN" else "Missing required property"
                lines.append((label, f"- {label} {path}: {prefix}.{google_required_suffix}{shape_str}"))
            elif "Value does not conform to Shape" in msg:
                fallback = "Value does not satisfy the required shape."
                if shape:
                    shape_msg = report_graph.value(shape, _SH.message)
                    if shape_msg:
                        fallback = str(shape_msg)
                    else:
                        hint = _or_shape_hint(report_graph, shape)
                        if hint:
                            fallback = hint
                if path:
                    lines.append((label, f"- {label} {path}: {fallback}{value_str}{shape_str}"))
                else:
                    lines.append((label, f"- {label} {fallback}{value_str}{focus_str}{shape_str}"))
            elif "must conform to one or more shapes" in msg:
                fallback = "Failed to satisfy one of the alternative constraints."
                if shape:
                    shape_msg = report_graph.value(shape, _SH.message)
                    if shape_msg:
                        fallback = str(shape_msg)
                    else:
                        hint = _or_shape_hint(report_graph, shape)
                        if hint:
                            fallback = hint
                if path:
                    lines.append((label, f"- {label} {path}: {fallback}{value_str}{shape_str}"))
                else:
                    lines.append((label, f"- {label} {fallback}{value_str}{focus_str}{shape_str}"))
            elif path and msg:
                lines.append((label, f"- {label} {path}: {msg}{value_str}{shape_str}"))
            elif msg:
                lines.append((label, f"- {label} {msg}{value_str}{focus_str}{shape_str}"))
            else:
                lines.append((label, f"- {label}"))
        lines.append(("text", ""))

    return header, lines


def _emit_validation_result(
    result,
    format: str,
    color: bool,
    report_file: str | None,
) -> None:
    if format not in {"pretty", "raw"}:
        raise UsageError("Invalid format. Use 'pretty' or 'raw'.")

    if format == "raw":
        output_text = result.report_text
        if report_file:
            Path(report_file).write_text(output_text, encoding="utf-8")
        else:
            typer.echo(output_text)
    else:
        header, lines = _format_pretty_lines(
            result.report_graph,
            result.data_graph,
            result.shape_source_map,
        )
        if report_file:
            output_text = "\n".join([header] + [line for _, line in lines]).rstrip() + "\n"
            Path(report_file).write_text(output_text, encoding="utf-8")
        else:
            status_color = {
                "Status: ERROR": typer.colors.RED,
                "Status: WARNING": typer.colors.YELLOW,
                "Status: INFO": typer.colors.BLUE,
                "Status: OK": typer.colors.GREEN,
            }
            header_color = None
            for prefix, fg in status_color.items():
                if header.startswith(prefix):
                    header_color = fg
                    break
            typer.secho(header, fg=header_color if color else None)
            for label, line in lines:
                if not line:
                    typer.echo("")
                    continue
                fg = None
                if color:
                    if label == "ERROR":
                        fg = typer.colors.RED
                    elif label == "WARN":
                        fg = typer.colors.YELLOW
                    elif label == "INFO":
                        fg = typer.colors.BLUE
                typer.secho(line, fg=fg)

    if not result.conforms:
        raise UsageError("SHACL validation failed.")
    if result.warning_count:
        raise UsageError("SHACL validation warnings found.")

    typer.echo("OK: no errors or warnings.")


def _warn_deprecated(command_hint: str) -> None:
    typer.secho(
        "Warning: `worai validate` is deprecated. "
        f"Use `{command_hint}` instead.",
        err=True,
        fg=typer.colors.YELLOW,
    )


def _is_url(value: str) -> bool:
    return value.startswith("http://") or value.startswith("https://")


class _ValidateGroup(TyperGroup):
    def resolve_command(self, ctx, args):
        try:
            return super().resolve_command(ctx, args)
        except click.UsageError:
            if not args:
                raise
            args.insert(0, "legacy")
            return super().resolve_command(ctx, args)


app = typer.Typer(add_completion=False, no_args_is_help=True, cls=_ValidateGroup)


def _run_jsonld_validation(
    input_file: str,
    shape: list[str] | None,
    report_file: str | None,
    format: str,
    color: bool,
) -> None:
    result = _validate_jsonld_input(input_file, shape)
    _emit_validation_result(result, format=format, color=color, report_file=report_file)


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    list_shapes: bool = typer.Option(
        False,
        "--list-shapes",
        help="List available packaged shapes and exit.",
    ),
) -> None:
    if list_shapes:
        _list_shapes()
        raise typer.Exit()
    if ctx.invoked_subcommand is None:
        raise UsageError("Missing input file or URL.")


@app.command(
    "jsonld",
    no_args_is_help=True,
    help="Deprecated. Use `worai graph validate` for RDF files/URLs.",
)
def jsonld(
    input_file: str = typer.Argument(
        ...,
        help="Path or URL to JSON-LD input.",
    ),
    shape: list[str] | None = typer.Option(
        None,
        "--shape",
        "-s",
        help="Shape path or packaged shape name (default: all packaged shapes).",
        show_default=False,
    ),
    report_file: str | None = typer.Option(
        None,
        "--report-file",
        help="Write the SHACL report text to this file.",
    ),
    format: str = typer.Option(
        "pretty",
        "--format",
        help="Output format: pretty or raw.",
        show_default=True,
    ),
    color: bool = typer.Option(
        True,
        "--color/--no-color",
        help="Colorize pretty output.",
        show_default=True,
    ),
) -> None:
    _warn_deprecated("worai graph validate <file-or-url> [<file-or-url> ...]")
    _run_jsonld_validation(input_file, shape, report_file, format, color)


def _list_shapes() -> None:
    from wordlift_sdk.validation import list_shape_names

    for name in list_shape_names():
        typer.echo(name)


def _validate_jsonld_input(input_value: str, shape: list[str] | None):
    try:
        return validate_file(input_value, shape)
    except Exception as exc:  # pragma: no cover - depends on SDK error detail
        if _is_url(input_value):
            raise UsageError(
                "Failed to validate URL as JSON-LD. "
                "If this is a webpage URL, use `worai structured-data validate page <url>`."
            ) from exc
        raise


@app.command("legacy", hidden=True, no_args_is_help=True)
def legacy(
    input_file: str = typer.Argument(
        ...,
        help="Deprecated. Use `worai validate jsonld` instead.",
    ),
    shape: list[str] | None = typer.Option(
        None,
        "--shape",
        "-s",
        help="Shape path or packaged shape name (default: all packaged shapes).",
        show_default=False,
    ),
    report_file: str | None = typer.Option(
        None,
        "--report-file",
        help="Write the SHACL report text to this file.",
    ),
    format: str = typer.Option(
        "pretty",
        "--format",
        help="Output format: pretty or raw.",
        show_default=True,
    ),
    color: bool = typer.Option(
        True,
        "--color/--no-color",
        help="Colorize pretty output.",
        show_default=True,
    ),
) -> None:
    _warn_deprecated("worai graph validate <file-or-url> [<file-or-url> ...]")
    _run_jsonld_validation(input_file, shape, report_file, format, color)
