"""Public API for the Yagra package."""

from __future__ import annotations

import argparse
import dataclasses
import json
import sys
from collections.abc import Mapping
from os import PathLike
from pathlib import Path
from typing import Any

from langgraph.graph.state import CompiledStateGraph

from yagra.adapters.inbound import create_workflow_studio_server
from yagra.adapters.outbound import InMemoryNodeRegistry
from yagra.application.services.template_initializer import (
    FileAlreadyExistsError,
    TemplateNotFoundError,
    initialize_from_template,
    list_templates,
)
from yagra.application.use_cases import (
    build_from_workflow_path,
    format_validation_report,
    render_workflow_visualization_html,
    validate_workflow_for_ui,
)
from yagra.domain.entities import GraphSpec
from yagra.ports.outbound import NodeHandler, NodeRegistryPort


class Yagra:
    """Wrapper for executing LangGraph built from workflow YAML."""

    def __init__(self, compiled_graph: CompiledStateGraph) -> None:
        """Initializes with a compiled graph.

        Args:
            compiled_graph: Compiled graph generated by `build_state_graph`.
        """
        self._compiled_graph = compiled_graph

    @classmethod
    def from_workflow(
        cls,
        workflow_path: str | PathLike[str],
        registry: NodeRegistryPort | Mapping[str, NodeHandler],
        bundle_root: str | PathLike[str] | None = None,
        state_schema: Any = dict,
    ) -> Yagra:
        """Creates a `Yagra` instance from a workflow file.

        Args:
            workflow_path: Path to the entry `workflow.yaml`.
            registry: Registry implementation resolving handler names to callables, or handler mapping.
            bundle_root: Base directory for resolving split references. Defaults to workflow parent directory if not specified.
            state_schema: LangGraph state schema. Defaults to `dict`.

        Returns:
            `Yagra` instance containing the compiled graph.
        """
        normalized_registry = _normalize_registry(registry)
        compiled = build_from_workflow_path(
            workflow_path=workflow_path,
            registry=normalized_registry,
            bundle_root=bundle_root,
            state_schema=state_schema,
        )
        return cls(compiled)

    @property
    def compiled_graph(self) -> CompiledStateGraph:
        """Returns the held compiled graph."""
        return self._compiled_graph

    def invoke(self, state: Mapping[str, Any]) -> dict[str, Any]:
        """Executes the graph with an initial state.

        Args:
            state: State dictionary at execution start.

        Returns:
            State dictionary after execution.

        Raises:
            TypeError: If the graph execution result is not dict-compatible.
        """
        result = self._compiled_graph.invoke(dict(state))
        if not isinstance(result, Mapping):
            raise TypeError("compiled graph returned non-mapping result")
        return dict(result)


def main() -> None:
    """CLI entry point for Yagra."""
    parser = _build_cli_parser()
    args = parser.parse_args()

    if args.command == "init":
        exit_code = _run_init_command(args)
        raise SystemExit(exit_code)
    if args.command == "schema":
        exit_code = _run_schema_command(args)
        raise SystemExit(exit_code)
    if args.command == "validate":
        exit_code = _run_validate_command(args)
        raise SystemExit(exit_code)
    if args.command == "visualize":
        exit_code = _run_visualize_command(args)
        raise SystemExit(exit_code)
    if args.command == "studio":
        exit_code = _run_studio_command(args)
        raise SystemExit(exit_code)
    raise SystemExit(2)


def _normalize_registry(registry: NodeRegistryPort | Mapping[str, NodeHandler]) -> NodeRegistryPort:
    """Normalizes the registry argument to a `NodeRegistryPort` implementation.

    Args:
        registry: Registry implementation or handler mapping.

    Returns:
        `NodeRegistryPort` implementation.

    Raises:
        TypeError: If an unsupported type is passed.
    """
    if isinstance(registry, NodeRegistryPort):
        return registry
    if isinstance(registry, Mapping):
        return InMemoryNodeRegistry(registry)
    raise TypeError("registry must be NodeRegistryPort or mapping[str, NodeHandler]")


def _run_init_command(args: argparse.Namespace) -> int:
    """Executes the `init` subcommand.

    Args:
        args: Parsed CLI arguments.

    Returns:
        Exit code. 0 on success.
    """
    # テンプレート一覧表示
    if args.list:
        templates = list_templates()
        if not templates:
            print("利用可能なテンプレートがありません。")
            return 1
        print("利用可能なテンプレート:")
        for template in templates:
            print(f"  - {template}")
        return 0

    # テンプレート名が指定されていない場合はエラー
    if args.template is None:
        print(
            "エラー: --template または --list のいずれかを指定してください。",
            file=sys.stderr,
        )
        return 2

    output_dir = Path(args.output).expanduser().resolve()

    try:
        initialize_from_template(
            template_name=args.template,
            output_dir=output_dir,
            force=args.force,
        )
    except TemplateNotFoundError as e:
        print(str(e), file=sys.stderr)
        return 1
    except FileAlreadyExistsError as e:
        print(str(e), file=sys.stderr)
        return 1

    print(f"テンプレート '{args.template}' から初期化しました: {output_dir}")

    # 生成されたワークフローを検証
    workflow_path = output_dir / "workflow.yaml"
    if workflow_path.exists():
        print(f"\nワークフローを検証しています: {workflow_path}")
        report = validate_workflow_for_ui(
            workflow_path=str(workflow_path),
            bundle_root=None,
        )
        if report.is_valid:
            print("✓ ワークフローは valid です。")
        else:
            print("✗ ワークフローに問題があります:", file=sys.stderr)
            print(format_validation_report(report), file=sys.stderr)
            return 1

    return 0


def _build_cli_parser() -> argparse.ArgumentParser:
    """Builds the CLI parser.

    Returns:
        Instance of `argparse.ArgumentParser`.
    """
    parser = argparse.ArgumentParser(prog="yagra")
    subparsers = parser.add_subparsers(dest="command", required=True)

    init = subparsers.add_parser(
        "init",
        help="テンプレートからワークフローを初期化する",
    )
    init.add_argument(
        "--template",
        default=None,
        help="使用するテンプレート名（例: branch, loop, rag）",
    )
    init.add_argument(
        "--list",
        action="store_true",
        help="利用可能なテンプレート一覧を表示する",
    )
    init.add_argument(
        "--force",
        action="store_true",
        help="既存ファイルを上書きする",
    )
    init.add_argument(
        "--output",
        default=".",
        help="出力先ディレクトリ（デフォルト: カレントディレクトリ）",
    )

    schema = subparsers.add_parser(
        "schema",
        help="workflow YAML の JSON Schema を出力する",
    )
    schema.add_argument(
        "--output",
        default=None,
        help="出力ファイルパス（未指定時は標準出力）",
    )

    validate = subparsers.add_parser(
        "validate",
        help="workflow YAML を検証する",
    )
    validate.add_argument(
        "--workflow",
        required=True,
        help="検証対象 workflow YAML のパス",
    )
    validate.add_argument(
        "--bundle-root",
        default=None,
        help="分割参照解決の基準ディレクトリ",
    )
    validate.add_argument(
        "--format",
        choices=["text", "json"],
        default="text",
        help="出力フォーマット (デフォルト: text)",
    )

    visualize = subparsers.add_parser(
        "visualize",
        help="workflow を Read Only HTML として可視化する",
    )
    visualize.add_argument(
        "--workflow",
        required=True,
        help="可視化対象 workflow YAML のパス",
    )
    visualize.add_argument(
        "--bundle-root",
        default=None,
        help="分割参照解決の基準ディレクトリ",
    )
    visualize.add_argument(
        "--output",
        default="workflow-visualization.html",
        help="生成する HTML ファイルパス",
    )
    visualize.add_argument(
        "--title",
        default=None,
        help="可視化ページのタイトル",
    )

    studio = subparsers.add_parser(
        "studio",
        help="workflow 編集用のローカル WebUI/API を起動する",
    )
    studio.add_argument(
        "--workflow",
        default=None,
        help="編集対象 workflow YAML のパス（未指定時は起動後にUIで選択/作成）",
    )
    studio.add_argument(
        "--bundle-root",
        default=None,
        help="分割参照解決の基準ディレクトリ",
    )
    studio.add_argument(
        "--ui-state",
        default=None,
        help="UI サイドカー JSON のパス（未指定時は <workflow>.workflow-ui.json）",
    )
    studio.add_argument(
        "--workspace-root",
        default=None,
        help=(
            "Studio が workflow を探索/作成するワークスペースルート"
            "（未指定時は workflow がカレント配下ならカレント、"
            "それ以外は workflow 親、workflow 未指定時はカレント）"
        ),
    )
    studio.add_argument(
        "--backup-dir",
        default=".yagra/backups",
        help="バックアップ格納ディレクトリ",
    )
    studio.add_argument(
        "--host",
        default="127.0.0.1",
        help="バインドホスト",
    )
    studio.add_argument(
        "--port",
        type=int,
        default=8787,
        help="バインドポート",
    )

    return parser


def _run_schema_command(args: argparse.Namespace) -> int:
    """Executes the `schema` subcommand.

    Outputs the JSON Schema for GraphSpec.
    Provides schema information for coding agents to generate accurate workflow YAML.

    Args:
        args: Parsed CLI arguments.

    Returns:
        Exit code. 0 on success.
    """
    schema = GraphSpec.model_json_schema()
    schema_json = json.dumps(schema, indent=2, ensure_ascii=False)

    if args.output:
        output_path = Path(args.output).expanduser().resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(schema_json + "\n", encoding="utf-8")
        print(f"schema exported: {output_path}")
    else:
        print(schema_json)
    return 0


def _run_validate_command(args: argparse.Namespace) -> int:
    """Executes the `validate` subcommand.

    Validates workflow YAML and outputs the result in text or JSON format.
    JSON format can be used for error correction loops by coding agents.

    Args:
        args: Parsed CLI arguments.

    Returns:
        Exit code. valid → 0, invalid → 1.
    """
    report = validate_workflow_for_ui(
        workflow_path=args.workflow,
        bundle_root=args.bundle_root,
    )

    if args.format == "json":
        report_dict = {
            "is_valid": report.is_valid,
            "issues": [dataclasses.asdict(issue) for issue in report.issues],
        }
        print(json.dumps(report_dict, indent=2, ensure_ascii=False))
    else:
        print(
            format_validation_report(report), file=sys.stderr if not report.is_valid else sys.stdout
        )

    return 0 if report.is_valid else 1


def _run_visualize_command(args: argparse.Namespace) -> int:
    """Executes the `visualize` subcommand.

    Args:
        args: Parsed CLI arguments.

    Returns:
        Exit code. 0 on success.
    """
    report = validate_workflow_for_ui(
        workflow_path=args.workflow,
        bundle_root=args.bundle_root,
    )
    if not report.is_valid:
        print(format_validation_report(report), file=sys.stderr)
        return 1

    html_text = render_workflow_visualization_html(
        workflow_path=args.workflow,
        bundle_root=args.bundle_root,
        title=args.title,
    )

    output_path = Path(args.output).expanduser().resolve()
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(html_text, encoding="utf-8")

    print(f"workflow visualization generated: {output_path}")
    return 0


def _run_studio_command(args: argparse.Namespace) -> int:
    """Executes the `studio` subcommand.

    Args:
        args: Parsed CLI arguments.

    Returns:
        Exit code. 0 on success.
    """
    if args.workflow is None and args.ui_state is not None:
        print(
            "--ui-state は --workflow 指定時のみ利用できます。",
            file=sys.stderr,
        )
        return 2

    server = create_workflow_studio_server(
        workflow_path=args.workflow,
        bundle_root=args.bundle_root,
        ui_state_path=args.ui_state,
        workspace_root=args.workspace_root,
        backup_dir=args.backup_dir,
        host=args.host,
        port=args.port,
    )
    base_url = f"http://{args.host}:{args.port}"
    print(f"workflow studio started: {base_url}")
    print("press Ctrl+C to stop")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
    return 0


__all__ = ["Yagra", "main"]
