"""CyberBrain26 command line interface.

Every command in this module resolves its filesystem locations through
:mod:`cyberbrain26.product.local_store` — the single existing resolution path
that honours ``CYBERBRAIN_DATA_DIR`` / ``CYBERBRAIN_DB_PATH`` and creates the
data directory with mode ``0o700``. No command here computes a data directory
of its own, so ``roadmaps.json`` and the SQLite store can never diverge.

Execution model (decided by the product owner, 2 Oct 2026)
---------------------------------------------------------
``cyberbrain run`` routes every roadmap step through
:meth:`PtahBridge.execute_step`, which is the only entry that always applies
``InMemoryTaskDispatcher._authorize`` (identity -> scope -> policy -> approval)
and writes an audit record. There is **no model call anywhere in this path**:
a model turn has no action and no scope in the policy system, so it cannot be
authorized through ``AuthorizedExecutionService``, and executing model-generated
text would run untrusted input in the sandbox.

The direct ``ModelGateway.chat`` call that previously lived in ``run`` is gone,
and with it the ``--model`` and ``--lang`` options. As a direct consequence,
roadmaps produced by ``cyberbrain plan`` carry planner steps whose payload
declares no action; running such a roadmap executes nothing and reports every
step as refused. That is the accepted, visible consequence of the decision and
is stated by the command instead of being papered over.

Operator-facing output
---------------------
Diagnostics this module controls go to stderr via ``typer.echo(..., err=True)``
and every failure path exits non-zero. Progress lines go to stdout via
``rich.print``. This module never re-raises a handled exception, so a command
failure never prints a Python traceback of its own. The libraries below it do
log their own diagnostics (including stack traces) at ERROR level on stderr when
a step is denied; suppressing that would hide real operational detail, so it is
left in place.
"""

from __future__ import annotations

import asyncio
import json
from collections.abc import Sequence
from pathlib import Path
from typing import Annotated, Any

import typer
from rich import print

from cyberbrain26 import __version__
from cyberbrain26.control_plane import SQLiteTaskQueue
from cyberbrain26.data_plane.sqlite_store import SQLiteDatabase
from cyberbrain26.knowledge_plane.contracts import (
    DocumentTrust,
    KG1Node,
    KnowledgeSource,
    Sensitivity,
)
from cyberbrain26.knowledge_plane.embedding_config import EmbeddingConfig
from cyberbrain26.knowledge_plane.memory_service import MemoryService
from cyberbrain26.orchestration.ptah_bridge import (
    PtahBridge,
    Roadmap,
    RoadmapStatus,
    RoadmapStep,
    StepResult,
    StepStatus,
)
from cyberbrain26.product.approvals import approvals_app
from cyberbrain26.product.doctor import doctor_app
from cyberbrain26.product.local_store import ensure_data_dir, open_database, resolve_db_path
from cyberbrain26.security_plane.audit import AuditLog
from cyberbrain26.security_plane.identity import PermissionScope
from cyberbrain26.security_plane.permission import SecurityError

app = typer.Typer(name="cyberbrain", help="CyberBrain26 CLI Interface")

app.add_typer(approvals_app, name="approvals")
app.add_typer(doctor_app, name="doctor")

_DB_HELP = "Explicit path to the CyberBrain26 SQLite database file."
_DATA_DIR_HELP = "Explicit data directory holding the database file."
_APPROVAL_TIMEOUT_HELP = (
    "Seconds to wait for a human approval decision per step. Default 300 because the "
    "library default (30 s) is unusable for a two-terminal flow: on timeout the request "
    "is persisted as 'cancelled' and disappears from 'approvals pending'."
)
_SCOPES_HELP = (
    "Comma-separated permission scopes granted to the coder identity, e.g. "
    "'python,shell'. Default is empty, and an empty value grants nothing: a step "
    "carrying code then ends closed at the scope gate. "
    "LIMITATION: --scopes currently applies only to the 'coder' role; it is NOT "
    "applied to security, evaluator, research, browser or planner steps. A "
    "code-carrying step with a non-coder role is not granted these scope(s) and "
    "is denied at the scope gate. This is a known limitation of the current "
    "identity_scopes schema and may be widened in a later design review."
)

_ROADMAPS_FILENAME = "roadmaps.json"

# The library default is 30 s; a two-terminal approve-then-run flow needs longer.
_DEFAULT_APPROVAL_TIMEOUT_SECONDS: float = 300.0

# Read from the security plane instead of hardcoded, so the CLI can never offer a
# scope the identity layer does not know about.
_VALID_SCOPES: frozenset[str] = frozenset(scope.value for scope in PermissionScope)


# ----------------------------------------------------------------------
# Store resolution
# ----------------------------------------------------------------------
def _roadmaps_file(data_dir: Path) -> Path:
    """Return the roadmap index inside an already-resolved data directory."""
    return data_dir / _ROADMAPS_FILENAME


def _load_roadmaps(data_dir: Path) -> dict[str, Any]:
    """Read the roadmap index, or an empty mapping when nothing is stored yet.

    Raises:
        typer.Exit: With code 1 when the index exists but cannot be decoded.
            A corrupt index is refused rather than silently treated as empty,
            because "no roadmaps" and "unreadable roadmaps" must never look alike.
    """
    path = _roadmaps_file(data_dir)
    if not path.exists():
        return {}
    try:
        data = json.loads(path.read_text())
    except (OSError, ValueError) as exc:
        typer.echo(
            f"Roadmap index {path} could not be read: {exc}. Refusing to run; "
            "repair or move the file and retry.",
            err=True,
        )
        raise typer.Exit(1) from None
    if not isinstance(data, dict):
        typer.echo(
            f"Roadmap index {path} is not a JSON object of roadmaps. Refusing to run.",
            err=True,
        )
        raise typer.Exit(1)
    return data


def _save_roadmaps(data_dir: Path, data: dict[str, Any]) -> None:
    """Write the roadmap index into an already-created data directory.

    The directory is created by :func:`ensure_data_dir` with mode ``0o700``
    before this is reached; it is never recreated here.
    """
    _roadmaps_file(data_dir).write_text(json.dumps(data, indent=2))


def _open_database_or_fail(db: Path | None, data_dir: Path) -> SQLiteDatabase:
    """Open the persistent SQLite store shared by every execution path.

    Persistence is mandatory: approval requests and audit records written by
    ``run`` must be readable by a second terminal running ``approvals``, so a
    store-less run would be a silent pass. There is deliberately no
    ``--no-persist`` flag on ``plan`` or ``run``.

    Args:
        db: Optional explicit database file path.
        data_dir: Resolved data directory used to derive the default path.

    Returns:
        The opened database handle.

    Raises:
        typer.BadParameter: If no handle could be opened. Never falls back to
            an in-memory store.
    """
    database = open_database(db_path=db, data_dir=data_dir, persist=True)
    if database is None:
        raise typer.BadParameter(
            "Persistent storage is mandatory for this command, but no SQLite database "
            "could be opened. Fix --db/--data-dir and retry; refusing to run without "
            "persistence because approvals and audit records would be lost."
        )
    return database


def _build_bridge(
    *,
    data_dir: Path,
    db: Path | None,
    approval_timeout_seconds: float,
    coder_scopes: Sequence[str] = (),
) -> PtahBridge:
    """Build a SQLite-backed :class:`PtahBridge` on the single shared store.

    Args:
        data_dir: Resolved data directory holding ``roadmaps.json``.
        db: Optional explicit database file path.
        approval_timeout_seconds: How long the bridge waits for each approval.
        coder_scopes: Scopes merged into the coder identity. Empty means the
            identity keeps no scopes, so a code step is denied at the scope
            gate before policy and approval are consulted.

    Returns:
        A bridge whose task queue, audit log and approval manager all share the
        same database file, so a decision made in another process is visible.
    """
    database = _open_database_or_fail(db, data_dir)
    identity_scopes = {"coder": list(coder_scopes)} if coder_scopes else None
    return PtahBridge(
        database=database,
        approval_timeout_seconds=approval_timeout_seconds,
        identity_scopes=identity_scopes,
    )


# ----------------------------------------------------------------------
# Option parsing
# ----------------------------------------------------------------------
def _parse_scopes(raw: str) -> list[str]:
    """Parse and validate a ``--scopes`` CSV against the security plane's set.

    Args:
        raw: Raw option value; empty or ``None`` means "grant nothing".

    Returns:
        The validated, de-duplicated scope list, preserving the given order.

    Raises:
        typer.BadParameter: If any entry is not a scope the identity layer
            defines. Guessing is refused instead of silently dropping values.
    """
    if not raw:
        return []
    scopes: list[str] = []
    for item in raw.split(","):
        scope = item.strip()
        if scope and scope not in scopes:
            scopes.append(scope)
    unknown = [scope for scope in scopes if scope not in _VALID_SCOPES]
    if unknown:
        raise typer.BadParameter(
            f"unknown scope(s) in --scopes: {', '.join(unknown)}. "
            f"Valid scopes are: {', '.join(sorted(_VALID_SCOPES))}."
        )
    return scopes


def _validate_approval_timeout(value: float) -> float:
    """Reject a non-positive approval timeout before it reaches the bridge.

    Raises:
        typer.BadParameter: If the timeout is not a positive number.
    """
    if value <= 0:
        raise typer.BadParameter(
            f"--approval-timeout must be a positive number of seconds, got {value}."
        )
    return value


def _declared_action(step: RoadmapStep) -> str | None:
    """Return the step's explicitly declared action, or ``None`` when absent.

    No default is inferred. The dispatcher would silently fall back to
    ``"execute"`` for an absent action; the CLI refuses instead, so the operator
    sees which step forgot to declare one.
    """
    action = step.payload.get("action")
    if isinstance(action, str) and action.strip():
        return action.strip()
    return None


# ----------------------------------------------------------------------
# Roadmap serialisation
# ----------------------------------------------------------------------
def _serialize_roadmap(roadmap: Roadmap) -> dict[str, Any]:
    """Render a roadmap into the JSON-compatible form stored in the index.

    ``depends_on`` is part of the persisted shape on purpose: dropping it here
    would make a stored roadmap claim to be dependency-free when it is not.

    Args:
        roadmap: The roadmap to render.

    Returns:
        A mapping that round-trips through :func:`_deserialize_roadmap`.
    """
    return {
        "id": roadmap.id,
        "name": roadmap.name,
        "steps": [
            {
                "id": s.id,
                "role": s.role,
                "description": s.description,
                "payload": s.payload,
                "depends_on": s.depends_on,
                "status": StepStatus(s.status).value,
            }
            for s in roadmap.steps
        ],
        "status": RoadmapStatus(roadmap.status).value,
    }


def _deserialize_roadmap(data: dict[str, Any]) -> Roadmap:
    """Rebuild a roadmap from its stored JSON form.

    Args:
        data: One entry of the ``roadmaps.json`` index.

    Returns:
        The roadmap, with step identity and ``depends_on`` preserved.

    Raises:
        ValueError: If a step or the roadmap carries an unknown status value.
        KeyError: If the required ``id``, ``name`` or step keys are missing.
    """
    steps = [
        RoadmapStep(
            id=s["id"],
            role=s["role"],
            description=s["description"],
            payload=s["payload"],
            depends_on=s.get("depends_on", []),
            status=StepStatus(s["status"]) if s.get("status") else StepStatus.PENDING,
        )
        for s in data.get("steps", [])
    ]
    return Roadmap(
        id=data["id"],
        name=data["name"],
        steps=steps,
        status=RoadmapStatus(data.get("status", "pending")),
    )


def _load_roadmap_for_run(bridge: PtahBridge, data_dir: Path, roadmap_id: str) -> Roadmap:
    """Resolve a roadmap from the live bridge or from the stored index.

    Args:
        bridge: The bridge the roadmap will be executed through.
        data_dir: Resolved data directory holding ``roadmaps.json``.
        roadmap_id: Roadmap identifier supplied on the command line.

    Returns:
        The roadmap, re-registered on ``bridge`` so ``execute_step`` can find it.

    Raises:
        typer.Exit: With code 1 when no such roadmap exists anywhere.
    """
    roadmap = bridge.get_roadmap(roadmap_id)
    if roadmap is not None:
        return roadmap
    stored = _load_roadmaps(data_dir)
    if roadmap_id not in stored:
        typer.echo(
            f"Roadmap '{roadmap_id}' was not found in {_roadmaps_file(data_dir)}. "
            "Create it with 'cyberbrain plan' first.",
            err=True,
        )
        raise typer.Exit(1)
    try:
        roadmap = _deserialize_roadmap(stored[roadmap_id])
        # Re-register through the public API so no private state is touched.
        # create_roadmap keeps the very same step objects, so step identity and
        # depends_on survive the round trip; only the roadmap status is reset,
        # which is irrelevant here because a run decides success per step.
        return bridge.create_roadmap(roadmap.id, roadmap.name, roadmap.steps)
    except (KeyError, TypeError, ValueError) as exc:
        typer.echo(
            f"Roadmap '{roadmap_id}' in {_roadmaps_file(data_dir)} is malformed "
            f"({type(exc).__name__}: {exc}). Refusing to run it; no step was dispatched.",
            err=True,
        )
        raise typer.Exit(1) from None


# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@app.command()
def version() -> None:
    """Display current CyberBrain26 version."""
    print(f"[bold green]CyberBrain26[/bold green] version: [cyan]{__version__}[/cyan]")


@app.command()
def plan(
    goal: str = typer.Option(..., "--goal", "-g", help="The high-level goal to plan."),
    roadmap_id: str = typer.Option("default", "--id", help="Roadmap identifier."),
    steps: int = typer.Option(3, "--steps", "-n", help="Number of planning steps."),
    data_dir: Annotated[
        Path | None, typer.Option("--data-dir", help=_DATA_DIR_HELP)
    ] = None,
    db: Annotated[Path | None, typer.Option("--db", help=_DB_HELP)] = None,
) -> None:
    """Generate a roadmap plan from a goal using the PlannerAgent.

    Planning is not an execution path: the planner only builds an in-memory task
    list, so there is nothing to authorize here. The generated roadmap still
    carries action-less steps, which 'run' refuses. Exits non-zero when
    planning fails.
    """
    resolved_data_dir = ensure_data_dir(data_dir)
    bridge = _build_bridge(
        data_dir=resolved_data_dir,
        db=db,
        approval_timeout_seconds=_DEFAULT_APPROVAL_TIMEOUT_SECONDS,
    )
    # Discard any stale durable result left by a previous ``plan --id <id>`` so
    # the planner step is freshly dispatched instead of returning the old plan.
    # Planning is not execution: the durable task stores a planner result, and
    # ``execute_step`` would otherwise restore that stale COMPLETED row and skip
    # the planner entirely. ``bridge.queue`` is always a SQLiteTaskQueue here
    # because ``_build_bridge`` refuses to run without persistence.
    durable_id = f"ptah:{roadmap_id}:step-0"
    if isinstance(bridge.queue, SQLiteTaskQueue):
        bridge.queue.delete_job(durable_id)
    result = asyncio.run(bridge.execute_step(
        roadmap_id,
        RoadmapStep(
            id="step-0",
            role="planner",
            description=f"Plan for goal: {goal}",
            # The dispatcher refuses a payload with no action, so the planner
            # states its own: "plan" is the registered non-mutating planner
            # action. Nothing is inferred on the caller's behalf.
            payload={"goal": goal, "max_steps": steps, "action": "plan"},
        ),
    ))
    if result.success:
        plan_data = result.data.get("plan", [])
        step_records = [
            RoadmapStep(
                id=f"step-{i}",
                role="planner",
                description=s.get("description", ""),
                payload={"goal": goal, "sub_task": s.get("description", "")},
            )
            for i, s in enumerate(plan_data, start=1)
        ]
        roadmap = bridge.create_roadmap(roadmap_id, goal, step_records)
        roadmaps = _load_roadmaps(resolved_data_dir)
        roadmaps[roadmap_id] = _serialize_roadmap(roadmap)
        _save_roadmaps(resolved_data_dir, roadmaps)
        print(f"[bold green]Roadmap '{roadmap_id}' created with {len(step_records)} steps[/bold green]")
        for s in roadmap.steps:
            print(f"  - {s.id}: {s.description}")
    else:
        # An error goes to stderr and changes the exit code: reporting a failed
        # plan on stdout with exit 0 would let a caller mistake it for success.
        typer.echo(
            f"Planning failed for roadmap '{roadmap_id}': "
            f"{result.error or 'no error detail returned'}. No roadmap was written.",
            err=True,
        )
        raise typer.Exit(1)


def _report_step_result(step: RoadmapStep, result: StepResult) -> bool:
    """Print one step outcome; return True when it succeeded."""
    if result.success:
        print(f"[green]{step.id}[/green]: action '{_declared_action(step)}' completed")
        return True
    typer.echo(
        f"Step '{step.id}' (action '{_declared_action(step)}') was refused or failed: "
        f"{result.error or 'no error detail returned'}. Nothing is assumed to have run; "
        "inspect 'approvals pending' and the audit log.",
        err=True,
    )
    return False


@app.command()
def run(
    roadmap_id: str = typer.Option(..., "--id", "-i", help="Roadmap identifier to execute."),
    data_dir: Annotated[
        Path | None, typer.Option("--data-dir", help=_DATA_DIR_HELP)
    ] = None,
    db: Annotated[Path | None, typer.Option("--db", help=_DB_HELP)] = None,
    approval_timeout: Annotated[
        float, typer.Option("--approval-timeout", help=_APPROVAL_TIMEOUT_HELP)
    ] = _DEFAULT_APPROVAL_TIMEOUT_SECONDS,
    scopes: Annotated[str, typer.Option("--scopes", help=_SCOPES_HELP)] = "",
):
    """Execute a roadmap's explicitly declared actions through the security plane.

    No model is called. Each step is dispatched via ``PtahBridge.execute_step``,
    which applies identity, scope, policy and approval before the agent runs,
    and records an audit entry either way. A step whose payload declares no
    ``action`` is refused rather than executed under an inferred default, and a
    step whose ``depends_on`` did not succeed is blocked rather than dispatched
    on a broken premise.
    """
    resolved_data_dir = ensure_data_dir(data_dir)
    db_path = resolve_db_path(db, data_dir=resolved_data_dir)
    coder_scopes = _parse_scopes(scopes)
    timeout_seconds = _validate_approval_timeout(approval_timeout)
    bridge = _build_bridge(
        data_dir=resolved_data_dir,
        db=db,
        approval_timeout_seconds=timeout_seconds,
        coder_scopes=coder_scopes,
    )
    typer.echo(
        f"Roadmap '{roadmap_id}' on {db_path}; coder scopes: "
        f"{', '.join(coder_scopes) if coder_scopes else '(none)'}."
    )
    roadmap = _load_roadmap_for_run(bridge, resolved_data_dir, roadmap_id)

    if not roadmap.steps:
        # Nothing ran, so nothing may be reported as an authorized run.
        typer.echo(
            f"Roadmap '{roadmap_id}' in {_roadmaps_file(resolved_data_dir)} contains no "
            "steps. Nothing was executed.",
            err=True,
        )
        raise typer.Exit(1)

    failures: list[str] = []
    succeeded: set[str] = set()
    blocked: set[str] = set()

    async def _execute() -> None:
        for step in roadmap.steps:
            if _declared_action(step) is None:
                step.status = StepStatus.FAILED
                typer.echo(
                    f"Step '{step.id}' declares no 'action' in its payload, so there is "
                    "nothing to authorize. It was refused; add an explicit action to the "
                    "step payload and try again. No default action is ever inferred.",
                    err=True,
                )
                failures.append(step.id)
                continue
            unmet = sorted(set(step.depends_on) - succeeded)
            if unmet:
                # Dependencies are honoured: a step is never dispatched on the
                # premise of a predecessor that did not succeed.
                step.status = StepStatus.BLOCKED
                typer.echo(
                    f"Step '{step.id}' depends on {', '.join(unmet)}, which did not "
                    "succeed in this run. It was not dispatched.",
                    err=True,
                )
                blocked.add(step.id)
                failures.append(step.id)
                continue
            result = await bridge.execute_step(roadmap_id, step)
            if _report_step_result(step, result):
                succeeded.add(step.id)
            else:
                failures.append(step.id)

    try:
        asyncio.run(_execute())
    except (SecurityError, RuntimeError, OSError, ValueError) as exc:
        # Defensive net for paths outside execute_step, which returns its own
        # failures as a StepResult. The command exits non-zero and prints only
        # the message below; it never re-raises, so no Python traceback is
        # produced *by this command*. (The libraries still log their own
        # diagnostics, including stack traces, on stderr - see the note in the
        # module docstring.)
        typer.echo(f"Roadmap '{roadmap_id}' aborted: {exc}", err=True)
        raise typer.Exit(1) from None

    if failures:
        detail = (
            f" Of those, {len(blocked)} step(s) were never dispatched because their "
            "dependencies did not succeed."
            if blocked
            else ""
        )
        typer.echo(
            f"Roadmap '{roadmap_id}' did NOT complete: {len(failures)} of "
            f"{len(roadmap.steps)} step(s) failed ({', '.join(sorted(failures))})."
            f"{detail}",
            err=True,
        )
        raise typer.Exit(1)
    typer.echo(
        f"Roadmap '{roadmap_id}' completed: {len(roadmap.steps)} step(s) executed and "
        "authorized. No model was called; only declared actions ran."
    )


# ----------------------------------------------------------------------
# Knowledge commands (Phase 4 — Jurassic-pattern CLI)
# ----------------------------------------------------------------------


def _get_memory_service() -> MemoryService:
    """Vytvoří MemoryService s výchozími komponentami."""

    # Použijeme resolve_db_path z existujícího produktu, aby cesty
    # vždy splňovaly předpoklady bezpečnostního modelu.
    data_dir = ensure_data_dir(None)
    db_path = resolve_db_path(data_dir=data_dir)

    config = EmbeddingConfig()
    config.lancedb_directory.mkdir(parents=True, exist_ok=True)

    # Vytvoříme audit log a database
    audit_log = AuditLog()

    database = SQLiteDatabase(db_path)

    # Vytvoříme MemoryService
    memory_service = MemoryService(
        database=database,
        audit_log=audit_log,
        config=config,
    )

    return memory_service

@app.command()
def knowledge(
    action: str = typer.Option(..., "--action", help="Action: ingest, search, hybrid-search, graph-search, or kg1"),
    path: str | None = typer.Option(None, "--path", help="Path for ingest/ingest-directory"),
    query: str | None = typer.Option(None, "--query", help="Query for search"),
    kg1_type: str = typer.Option("topic", "--kg1-type", help="Type for KG1 node"),
    business_semantics: str = typer.Option(None, "--business-semantics", help="Business semantics for KG1 node"),
    domain_constraints: str = typer.Option(None, "--domain-constraints", help="Domain constraints for KG1 node"),
    auto_classify: bool = typer.Option(True, "--auto-classify/--no-auto-classify", help="Enable/disable auto-classification during ingest"),
    validate_stale: bool = typer.Option(True, "--validate-stale/--no-validate-stale", help="Filter out superseded documents during search"),
    timeline: bool = typer.Option(False, "--timeline", help="Show document timeline with valid_from/valid_to/superseded_by"),
    traversal: str | None = typer.Option(None, "--traversal", help="Entity traversal: neighbors, path, or related"),
    depth: int = typer.Option(2, "--depth", help="Hloubka procházení grafu pro graph-search"),
):
    """Knowledge plane entry: ingest directory, search embeddings, or register KG1 node."""
    if action == "ingest" and path is None:
        typer.echo("--path is required for 'ingest'", err=True)
        raise typer.Exit(1)
    if action == "search" and query is None:
        typer.echo("--query is required for 'search'", err=True)
        raise typer.Exit(1)
    if action == "hybrid-search" and query is None:
        typer.echo("--query is required for 'hybrid-search'", err=True)
        raise typer.Exit(1)
    
    memory_service = _get_memory_service()
    
    if action == "ingest":
        # Pro ingest použijeme register_file pro celý adresář
        assert path is not None
        typer.echo(f"Ingestování adresáře: {path}")
        if auto_classify:
            typer.echo("Auto-classifikace: zapnuta (entitace, typologie)")
        try:
            # Pro jednoduchost pro teď prochazíme soubory a registrujeme každý
            root_path = Path(path).resolve()
            files_processed = 0
            files_skipped = 0
            
            for file_path in root_path.rglob("*"):
                if file_path.is_file() and not file_path.name.startswith("."):
                    try:
                        _document, created = memory_service.register_file(file_path)
                        if created:
                            typer.echo(f"  Zaregistrován nový soubor: {file_path.name}")
                            files_processed += 1
                        else:
                            typer.echo(f"  Soubor již existuje (přeskočen): {file_path.name}")
                            files_skipped += 1
                    except OSError as e:
                        typer.echo(f"  Chyba při zpracování {file_path.name}: {e}", err=True)
                        files_skipped += 1
            
            typer.echo(f"Kompletně zpracováno {files_processed} nových souborů, {files_skipped} přeskočeno.")
            
        except OSError as e:
            typer.echo(f"Chyba při ingestování: {e}", err=True)
            raise typer.Exit(1) from None
            
    elif action in ("search", "hybrid-search"):
        # Použijeme semantic_search (pro "search") nebo hybrid_search (pro "hybrid-search")
        assert query is not None
        typer.echo(f"Hledání s dotazem: {query}")
        if validate_stale:
            typer.echo("Staleness validace: zapnuta (výsledky bez platných entit budou vynechány)")
        try:
            if action == "hybrid-search":
                results = memory_service.hybrid_search(query, limit=10)
                typer.echo("Hybrid search: keyword + semantic + RRF fusion")
            else:
                results = memory_service.semantic_search(query, limit=10)
            
            if timeline:
                # Show timeline information
                for result in results:
                    doc = memory_service.get_document(result.get("id", ""))
                    if doc:
                        typer.echo(f"  Dokument: {result.get('id')}")
                        typer.echo(f"    valid_from: {doc.valid_from}")
                        typer.echo(f"    valid_to: {doc.valid_to}")
                        typer.echo(f"    superseded_by: {doc.superseded_by or 'N/A'}")
            elif results:
                typer.echo(f"Nalezeno {len(results)} výsledků:")
                for i, result in enumerate(results, 1):
                    text = result.get("text", "")[:100] + "..." if len(result.get("text", "")) > 100 else result.get("text", "")
                    source_id = result.get("source_id", "")
                    path = result.get("path", "")
                    typer.echo(f"  {i}. {text}")
                    typer.echo(f"     ID: {result.get('id')}, Zdroj: {source_id}, Cesta: {path}")
            else:
                typer.echo("Žádné výsledky nenalezeny.")
                
        except OSError as e:
            typer.echo(f"Chyba při vyhledávání: {e}", err=True)
            raise typer.Exit(1) from None
            
    elif action == "graph-search":
        # Graph-based retrieval via entity relationship traversal (MemGraphRAG)
        assert query is not None
        typer.echo(f"Graph search pro entitu: {query}")
        try:
            results = memory_service.graph_search(query, limit=10, depth=depth)
            if timeline:
                for result in results:
                    doc = memory_service.get_document(result.get("id", ""))
                    if doc:
                        typer.echo(f"  Dokument: {result.get('id')}")
                        typer.echo(f"    valid_from: {doc.valid_from}")
                        typer.echo(f"    valid_to: {doc.valid_to}")
                        typer.echo(f"    superseded_by: {doc.superseded_by or 'N/A'}")
            elif results:
                typer.echo(f"Nalezeno {len(results)} výsledků (graph-based retrieval):")
                for i, result in enumerate(results, 1):
                    text = result.get("text", "")[:100] + "..." if len(result.get("text", "")) > 100 else result.get("text", "")
                    typer.echo(f"  {i}. {text}")
                    typer.echo(f"     ID: {result.get('id')}, Skóre: {result.get('score', 0):.3f}")
                    typer.echo(f"     Grafová vzdálenost: {result.get('graph_distance', 'N/A')}")
                    entities = result.get("matched_entities", [])
                    if entities:
                        typer.echo(f"     Matched entity: {', '.join(entities[:5])}")
            else:
                typer.echo("Žádné výsledky nenalezeny.")
        except OSError as e:
            typer.echo(f"Chyba při vyhledávání grafu: {e}", err=True)
            raise typer.Exit(1) from None
        # Zaregistrujeme KG1 node
        typer.echo(f"Registrační KG1 node typu: {kg1_type}")
        
        if not business_semantics:
            typer.echo("--business-semantics je povinné pro KG1 registraci", err=True)
            raise typer.Exit(1)
            
        try:
            # Vytvoříme KG1 node
            kg1_node = KG1Node(
                id=f"kg1-{kg1_type}-{hash(business_semantics)}",
                type=kg1_type,
                source=KnowledgeSource(
                    source_id="cli-input",
                    source_uri="cli://input",
                    trust=DocumentTrust.REVIEWED,
                    sensitivity=Sensitivity.PUBLIC,
                    approved=True,
                ),
                business_semantics={"primary": business_semantics},
                domain_constraints={"scope": domain_constraints or "general"},
                evidence=[],
            )
            
            registered_node = memory_service.register_kg1_node(kg1_node)
            typer.echo(f"KG1 node zaregistrován: {registered_node.id}")
            typer.echo(f"  Typ: {registered_node.type}")
            typer.echo(f"  Business semantics: {list(registered_node.business_semantics.keys())}")
            
        except OSError as e:
            typer.echo(f"Chyba při registraci KG1: {e}", err=True)
            raise typer.Exit(1) from None
            
    else:
        typer.echo(f"Neznámá akce: {action}", err=True)
        raise typer.Exit(1)


def main() -> None:
    app()


if __name__ == "__main__":
    main()
