"""Add file relationships in Hydrus based on relationship tags in sidecar."""

from __future__ import annotations

from typing import Any, Dict, Optional, Sequence
import re
from pathlib import Path
import sys

from SYS.logger import log
from SYS.item_accessors import get_sha256_hex, get_store_name
from PluginCore.registry import get_plugin

from SYS import pipeline as ctx
from .. import _shared as sh

Cmdlet = sh.Cmdlet
CmdletArg = sh.CmdletArg
SharedArgs = sh.SharedArgs
parse_cmdlet_args = sh.parse_cmdlet_args
normalize_result_input = sh.normalize_result_input
should_show_help = sh.should_show_help
get_field = sh.get_field

CMDLET = Cmdlet(
    name="add-relationship",
    summary=
    "Associate file relationships (king/alt/related) in Hydrus based on relationship tags in sidecar.",
    usage=
    "@1-3 | add-relationship -king @4    OR    add-relationship -path <file>    OR    @1,@2,@3 | add-relationship",
    arg=[
        CmdletArg(
            "path",
            type="string",
            description="Specify the local file path (if not piping a result).",
        ),
        SharedArgs.INSTANCE,
        SharedArgs.QUERY,
        CmdletArg(
            "-king",
            type="string",
            description=
            "Explicitly set the king hash/file for relationships (e.g., -king @4 or -king hash)",
        ),
        CmdletArg(
            "-alt",
            type="string",
            description=
            "Explicitly select alt item(s) by @ selection or hash list (e.g., -alt @3-5 or -alt <hash>,<hash>)",
        ),
        CmdletArg(
            "-type",
            type="string",
            description=
            "Relationship type for piped items (default: 'alt', options: 'king', 'alt', 'related')",
        ),
    ],
    detail=[
        "- Mode 1: Pipe multiple items, first becomes king, rest become alts (default)",
        "- Mode 2: Use -king to explicitly set which item/hash is the king: @1-3 | add-relationship -king @4",
        "- Mode 2b: Use -king and -alt to select both sides from the last table: add-relationship -king @1 -alt @3-5",
        "- Mode 3: Read relationships from sidecar tags:",
        "    - New format: 'relationship: <KING_HASH>,<ALT_HASH>,<ALT_HASH>' (first hash is king)",
        "    - Legacy:     'relationship: hash(king)<HASH>,hash(alt)<HASH>...'",
        "- Supports three relationship types: king (primary), alt (alternative), related (other versions)",
        "- When using -king, all piped items become the specified relationship type to the king",
    ],
)


_normalize_hash_hex = sh.normalize_hash


def _extract_relationships_from_tag(tag_value: str) -> Dict[str, list[str]]:
    """Parse relationship tags.

    Supported formats:
    - New:  relationship: <KING_HASH>,<ALT_HASH>,<ALT_HASH>
    - Old:  relationship: hash(king)<HASH>,hash(alt)<HASH>...

    Returns a dict like {"king": ["HASH1"], "alt": ["HASH2"], ...}
    """
    result: Dict[str,
                 list[str]] = {}
    if not isinstance(tag_value, str):
        return result

    # Match patterns like hash(king)HASH or hash(type)<HASH>
    pattern = r"hash\((\w+)\)<?([a-fA-F0-9]{64})>?"
    matches = re.findall(pattern, tag_value)

    if matches:
        for rel_type, hash_value in matches:
            normalized = _normalize_hash_hex(hash_value)
            if normalized:
                if rel_type not in result:
                    result[rel_type] = []
                result[rel_type].append(normalized)
        return result

    # New format: extract hashes, first is king
    hashes = re.findall(r"\b[a-fA-F0-9]{64}\b", tag_value)
    hashes = [h.strip().lower() for h in hashes if isinstance(h, str)]
    if not hashes:
        return result
    king = _normalize_hash_hex(hashes[0])
    if not king:
        return result
    result["king"] = [king]
    alts: list[str] = []
    for h in hashes[1:]:
        normalized = _normalize_hash_hex(h)
        if normalized and normalized != king:
            alts.append(normalized)
    if alts:
        result["alt"] = alts
    return result


def _apply_relationships_from_tags(
    relationship_tags: Sequence[str],
    *,
    hydrus_client: Any,
    use_local_storage: bool,
    local_storage_path: Optional[Path],
    config: Dict[str,
                 Any],
) -> int:
    """Persist relationship tags into Hydrus or local DB.

    Local DB semantics:
    - Treat the first hash (king) as the king.
    - Store directional alt -> king relationships (no reverse edge).
    """
    rel_tags = [
        t for t in relationship_tags
        if isinstance(t, str) and t.strip().lower().startswith("relationship:")
    ]
    if not rel_tags:
        return 0

    # Prefer Hydrus if available (hash-based relationships map naturally).
    if hydrus_client is not None and hasattr(hydrus_client, "set_relationship"):
        processed: set[tuple[str, str, str]] = set()
        for tag in rel_tags:
            rels = _extract_relationships_from_tag(tag)
            king = (rels.get("king") or [None])[0]
            if not king:
                continue
            king_norm = _normalize_hash_hex(king)
            if not king_norm:
                continue

            for rel_type in ("alt", "related"):
                for other in rels.get(rel_type, []) or []:
                    other_norm = _normalize_hash_hex(other)
                    if not other_norm or other_norm == king_norm:
                        continue
                    key = (other_norm, king_norm, rel_type)
                    if key in processed:
                        continue
                    try:
                        hydrus_client.set_relationship(other_norm, king_norm, rel_type)
                        processed.add(key)
                    except Exception:
                        pass
        return 0

    # Local DB fallback (store/hash-first)
    if use_local_storage and local_storage_path is not None:
        try:
            with API_folder_store(local_storage_path) as db:
                processed_pairs: set[tuple[str, str]] = set()
                for tag in rel_tags:
                    rels = _extract_relationships_from_tag(tag)
                    king = (rels.get("king") or [None])[0]
                    if not king:
                        continue
                    king_norm = _normalize_hash_hex(king)
                    if not king_norm:
                        continue

                    # For local DB we treat all non-king hashes as alts.
                    alt_hashes: list[str] = []
                    for bucket in ("alt", "related"):
                        alt_hashes.extend(
                            [h for h in (rels.get(bucket) or []) if isinstance(h, str)]
                        )

                    for alt in alt_hashes:
                        alt_norm = _normalize_hash_hex(alt)
                        if not alt_norm or alt_norm == king_norm:
                            continue
                        if (alt_norm, king_norm) in processed_pairs:
                            continue
                        db.set_relationship_by_hash(
                            alt_norm,
                            king_norm,
                            "alt",
                            bidirectional=False
                        )
                        processed_pairs.add((alt_norm, king_norm))
        except Exception:
            return 1
        return 0

    return 0


def _parse_at_selection(token: str) -> Optional[list[int]]:
    """Parse standard @ selection syntax into a list of 0-based indices.

    Supports: @2, @2-5, @{1,3,5}, @3,5,7, @3-6,8, @*
    """
    if not isinstance(token, str):
        return None
    t = token.strip()
    if not t.startswith("@"):
        return None
    if t == "@*":
        return []  # special sentinel: caller interprets as "all"

    selector = t[1:].strip()
    if not selector:
        return None
    if selector.startswith("{") and selector.endswith("}"):
        selector = selector[1:-1].strip()

    parts = [p.strip() for p in selector.split(",") if p.strip()]
    if not parts:
        return None

    indices_1based: set[int] = set()
    for part in parts:
        try:
            if "-" in part:
                start_s, end_s = part.split("-", 1)
                start = int(start_s.strip())
                end = int(end_s.strip())
                if start <= 0 or end <= 0 or start > end:
                    return None
                for i in range(start, end + 1):
                    indices_1based.add(i)
            else:
                num = int(part)
                if num <= 0:
                    return None
                indices_1based.add(num)
        except Exception:
            return None

    return sorted(i - 1 for i in indices_1based)


def _resolve_items_from_at(token: str) -> Optional[list[Any]]:
    """Resolve @ selection token into actual items from the current result context."""
    items = ctx.get_last_result_items()
    if not items:
        return None
    parsed = _parse_at_selection(token)
    if parsed is None:
        return None
    if token.strip() == "@*":
        return list(items)
    selected: list[Any] = []
    for idx in parsed:
        if 0 <= idx < len(items):
            selected.append(items[idx])
    return selected


def _extract_hash_and_store(item: Any) -> tuple[Optional[str], Optional[str]]:
    """Extract (hash_hex, store) from a result item (dict/object)."""
    try:
        return (
            get_sha256_hex(item, "hash_hex", "hash", "file_hash"),
            get_store_name(item, "store"),
        )
    except Exception:
        return None, None


def _hydrus_hash_exists(hydrus_client: Any, hash_hex: str) -> bool:
    """Best-effort check whether a hash exists in the connected Hydrus backend."""
    try:
        if hydrus_client is None or not hasattr(hydrus_client, "fetch_file_metadata"):
            return False
        payload = hydrus_client.fetch_file_metadata(
            hashes=[hash_hex],
            include_service_keys_to_tags=False,
            include_file_url=False,
            include_duration=False,
            include_size=False,
            include_mime=False,
            include_notes=False,
        )
        meta = payload.get("metadata") if isinstance(payload, dict) else None
        return bool(isinstance(meta, list) and meta)
    except Exception:
        return False


def _resolve_king_reference(king_arg: str) -> Optional[str]:
    """Resolve a king reference like '@4' to its actual hash.

    Store/hash mode intentionally avoids file-path dependency.
    """
    if not king_arg:
        return None

    # Check if it's already a valid hash
    normalized = _normalize_hash_hex(king_arg)
    if normalized:
        return normalized

    # Try to resolve as @ selection from pipeline context
    if king_arg.startswith("@"):
        selected = _resolve_items_from_at(king_arg)
        if not selected:
            log(f"Cannot resolve {king_arg}: no selection context", file=sys.stderr)
            return None
        if len(selected) != 1:
            log(
                f"{king_arg} selects {len(selected)} items; -king requires exactly 1",
                file=sys.stderr,
            )
            return None

        item = selected[0]
        item_hash = (
            get_field(item,
                      "hash_hex") or get_field(item,
                                               "hash") or get_field(item,
                                                                    "file_hash")
        )

        if item_hash:
            normalized = _normalize_hash_hex(str(item_hash))
            if normalized:
                return normalized

        log(f"Item {king_arg} has no hash information", file=sys.stderr)
        return None

    return None


def _refresh_relationship_view_if_current(
    target_hash: Optional[str],
    target_path: Optional[str],
    other: Optional[str],
    config: Dict[str,
                 Any],
) -> None:
    """If the current subject matches the target, refresh relationships via get-relationship."""
    try:
        from cmdlet import get as get_cmdlet  # type: ignore
    except Exception:
        return

    get_relationship = None
    try:
        get_relationship = get_cmdlet("get-relationship")
    except Exception:
        get_relationship = None
    if not callable(get_relationship):
        return

    try:
        subject = ctx.get_last_result_subject()
        if subject is None:
            return

        def norm(val: Any) -> str:
            return str(val).lower()

        target_hashes = [norm(v) for v in [target_hash, other] if v]
        target_paths = [norm(v) for v in [target_path, other] if v]

        subj_hashes: list[str] = []
        subj_paths: list[str] = []
        if isinstance(subject, dict):
            subj_hashes = [
                norm(v) for v in [
                    subject.get("hydrus_hash"),
                    subject.get("hash"),
                    subject.get("hash_hex"),
                    subject.get("file_hash"), ] if v
            ]
            subj_paths = [
                norm(v) for v in
                [subject.get("file_path"), subject.get("path"), subject.get("target")]
                if v
            ]
        else:
            subj_hashes = [
                norm(getattr(subject,
                             f,
                             None))
                for f in ("hydrus_hash", "hash", "hash_hex", "file_hash")
                if getattr(subject, f, None)
            ]
            subj_paths = [
                norm(getattr(subject,
                             f,
                             None)) for f in ("file_path", "path", "target")
                if getattr(subject, f, None)
            ]

        is_match = False
        if target_hashes and any(h in subj_hashes for h in target_hashes):
            is_match = True
        if target_paths and any(p in subj_paths for p in target_paths):
            is_match = True
        if not is_match:
            return

        refresh_args: list[str] = []
        if target_hash:
            refresh_args.extend(["-query", f"hash:{target_hash}"])
        get_relationship(subject, refresh_args, config)
    except Exception:
        pass


def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
    """Associate file relationships in Hydrus.

    Two modes of operation:
    1. Read from sidecar: Looks for relationship tags in the file's sidecar (format: "relationship: hash(king)<HASH>,hash(alt)<HASH>")
    2. Pipeline mode: When piping multiple results, the first becomes "king" and subsequent items become "alt"

    Returns 0 on success, non-zero on failure.
    """
    # Help
    if should_show_help(_args):
        log(f"Cmdlet: {CMDLET.name}\nSummary: {CMDLET.summary}\nUsage: {CMDLET.usage}")
        return 0

    # Parse arguments using CMDLET spec
    parsed = parse_cmdlet_args(_args, CMDLET)
    arg_path: Optional[Path] = None
    override_store = parsed.get("instance")
    override_hashes, query_valid = sh.require_hash_query(
        parsed.get("query"),
        "Invalid -query value (expected hash:<sha256>)",
        log_file=sys.stderr,
    )
    if not query_valid:
        return 1
    king_arg = parsed.get("king")
    alt_arg = parsed.get("alt")
    rel_type = parsed.get("type", "alt")

    raw_path = parsed.get("path")
    if raw_path:
        try:
            arg_path = Path(str(raw_path)).expanduser()
        except Exception:
            arg_path = Path(str(raw_path))

    # Handle @N selection which creates a list
    # Use normalize_result_input to handle both single items and lists
    items_to_process = normalize_result_input(result)

    # Allow selecting alt items directly from the last table via -alt @...
    # This enables: add-relationship -king @1 -alt @3-5
    if alt_arg:
        alt_text = str(alt_arg).strip()
        resolved_alt_items: list[Any] = []
        if alt_text.startswith("@"):
            selected = _resolve_items_from_at(alt_text)
            if not selected:
                log(
                    f"Failed to resolve -alt {alt_text}: no selection context",
                    file=sys.stderr
                )
                return 1
            resolved_alt_items = selected
        else:
            # Treat as comma/semicolon-separated list of hashes
            parts = [
                p.strip() for p in alt_text.replace(";", ",").split(",") if p.strip()
            ]
            hashes = [h for h in (_normalize_hash_hex(p) for p in parts) if h]
            if not hashes:
                log(
                    "Invalid -alt value (expected @ selection or 64-hex sha256 hash list)",
                    file=sys.stderr,
                )
                return 1
            if not override_store:
                log(
                    "-instance is required when using -alt with a raw hash list",
                    file=sys.stderr
                )
                return 1
            resolved_alt_items = [
                {
                    "hash": h,
                    "store": str(override_store)
                } for h in hashes
            ]
        items_to_process = normalize_result_input(resolved_alt_items)

    # Allow explicit store/hash-first operation via -query "hash:<sha256>" (supports multiple hash: tokens)
    if (not items_to_process) and override_hashes:
        if not override_store:
            log(
                "-instance is required when using -query without piped items",
                file=sys.stderr
            )
            return 1
        items_to_process = [
            {
                "hash": h,
                "store": str(override_store)
            } for h in override_hashes
        ]

    if not items_to_process and not arg_path:
        log(
            "No items provided to add-relationship (no piped result and no -path)",
            file=sys.stderr
        )
        return 1

    # If no items from pipeline, just process the -path arg
    if not items_to_process and arg_path:
        items_to_process = [{
            "file_path": arg_path
        }]

    # Resolve the king reference once (if provided)
    king_hash: Optional[str] = None
    king_store: Optional[str] = None
    if king_arg:
        king_text = str(king_arg).strip()
        if king_text.startswith("@"):
            selected = _resolve_items_from_at(king_text)
            if not selected:
                log(
                    f"Cannot resolve {king_text}: no selection context",
                    file=sys.stderr
                )
                return 1
            if len(selected) != 1:
                log(
                    f"{king_text} selects {len(selected)} items; -king requires exactly 1",
                    file=sys.stderr,
                )
                return 1
            king_hash, king_store = _extract_hash_and_store(selected[0])
            if not king_hash:
                log(f"Item {king_text} has no hash information", file=sys.stderr)
                return 1
        else:
            king_hash = _resolve_king_reference(king_text)
            if not king_hash:
                log(f"Failed to resolve king argument: {king_text}", file=sys.stderr)
                return 1

    # Decide target instance: override_store > (king store + piped item stores) (must be consistent)
    store_name: Optional[str] = str(override_store).strip() if override_store else None
    if not store_name:
        stores = set()
        if king_store:
            stores.add(str(king_store))
        for item in items_to_process:
            s = get_field(item, "store")
            if s:
                stores.add(str(s))
        if len(stores) == 1:
            store_name = next(iter(stores))
        elif len(stores) > 1:
            log(
                "Multiple stores detected (king/alt across stores); use -instance and ensure all selections are from the same store",
                file=sys.stderr,
            )
            return 1

    # Enforce same-instance relationships when store context is available.
    if king_store and store_name and str(king_store) != str(store_name):
        log(
            f"Cross-instance relationship blocked: king is in store '{king_store}' but -instance is '{store_name}'",
            file=sys.stderr,
        )
        return 1
    if store_name:
        for item in items_to_process:
            s = get_field(item, "store")
            if s and str(s) != str(store_name):
                log(
                    f"Cross-instance relationship blocked: alt item store '{s}' != '{store_name}'",
                    file=sys.stderr,
                )
                return 1

    # Resolve backend for store/hash operations
    backend = None
    is_folder_store = False
    store_root: Optional[Path] = None
    if store_name:
        backend, _store_registry, _exc = sh.get_store_backend(config, str(store_name))
        if backend is not None:
            if not bool(getattr(backend, "supports_relationship_association", False)):
                log(
                    f"Store '{store_name}' does not support relationships",
                    file=sys.stderr,
                )
                return 1
            loc = getattr(backend, "location", None)
            if callable(loc):
                is_folder_store = True
                store_root = Path(str(loc()))
        else:
            backend = None
            is_folder_store = False
            store_root = None

    # Select Hydrus client:
    # - If a store is specified and maps to a HydrusNetwork backend, use that backend's client.
    # - If no store is specified, use the default Hydrus client.
    # NOTE: When a store is specified, we do not fall back to a global/default Hydrus client.
    hydrus_client = None
    hydrus_provider = get_plugin("hydrusnetwork", config)
    if store_name and (not is_folder_store) and backend is not None:
        try:
            if hydrus_provider is not None:
                hydrus_client = hydrus_provider.get_client(
                    store_name=str(store_name),
                    allow_default=False,
                )
        except Exception:
            hydrus_client = None
    elif not store_name:
        try:
            if hydrus_provider is not None:
                hydrus_client = hydrus_provider.get_client()
        except Exception:
            hydrus_client = None

    # Use the selected store root when available; otherwise use the configured
    # local plugin root for sidecar/tag import lookup.
    from SYS.config import get_local_storage_path

    local_storage_root: Optional[Path] = None
    if store_root is not None:
        local_storage_root = store_root
    else:
        try:
            p = get_local_storage_path(config) if config else None
            local_storage_root = Path(p) if p else None
        except Exception:
            local_storage_root = None

    use_local_storage = local_storage_root is not None

    if king_hash:
        log(f"Using king hash: {king_hash}", file=sys.stderr)

    # If -path is provided, try reading relationship tags from its sidecar and persisting them.
    if arg_path is not None and arg_path.exists() and arg_path.is_file():
        try:
            sidecar_path = find_sidecar(arg_path)
            if sidecar_path is not None and sidecar_path.exists():
                _, tags, _ = read_sidecar(sidecar_path)
                relationship_tags = [
                    t for t in (tags or [])
                    if isinstance(t, str) and t.lower().startswith("relationship:")
                ]
                if relationship_tags:
                    code = _apply_relationships_from_tags(
                        relationship_tags,
                        hydrus_client=hydrus_client,
                        use_local_storage=use_local_storage,
                        local_storage_path=local_storage_root,
                        config=config,
                    )
                    return 0 if code == 0 else 1
        except Exception:
            pass

    # If piped items include relationship tags, persist them (one pass) then exit.
    try:
        rel_tags_from_pipe: list[str] = []
        for item in items_to_process:
            tags_val = None
            if isinstance(item, dict):
                tags_val = item.get("tag") or item.get("tags")
            else:
                tags_val = getattr(item, "tag", None)
            if isinstance(tags_val, list):
                rel_tags_from_pipe.extend(
                    [
                        t for t in tags_val
                        if isinstance(t, str) and t.lower().startswith("relationship:")
                    ]
                )
            elif isinstance(tags_val,
                            str) and tags_val.lower().startswith("relationship:"):
                rel_tags_from_pipe.append(tags_val)

        if rel_tags_from_pipe:
            code = _apply_relationships_from_tags(
                rel_tags_from_pipe,
                hydrus_client=hydrus_client,
                use_local_storage=use_local_storage,
                local_storage_path=local_storage_root,
                config=config,
            )
            return 0 if code == 0 else 1
    except Exception:
        pass

    # STORE/HASH MODE (preferred): use -instance and hashes; do not require file paths.
    if store_name and is_folder_store and store_root is not None:
        try:
            with API_folder_store(store_root) as db:
                # Mode 1: no explicit king -> first is king, rest are alts
                if not king_hash:
                    first_hash = None
                    for item in items_to_process:
                        h, item_store = _extract_hash_and_store(item)
                        if item_store and store_name and str(item_store) != str(
                                store_name):
                            log(
                                f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
                                file=sys.stderr,
                            )
                            return 1
                        if not h:
                            continue
                        if not first_hash:
                            first_hash = h
                            continue
                        # directional alt -> king by default for local DB
                        bidirectional = str(rel_type).lower() != "alt"
                        db.set_relationship_by_hash(
                            h,
                            first_hash,
                            str(rel_type),
                            bidirectional=bidirectional
                        )
                    return 0

                # Mode 2: explicit king
                for item in items_to_process:
                    h, item_store = _extract_hash_and_store(item)
                    if item_store and store_name and str(item_store) != str(store_name):
                        log(
                            f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
                            file=sys.stderr,
                        )
                        return 1
                    if not h or h == king_hash:
                        continue
                    bidirectional = str(rel_type).lower() != "alt"
                    db.set_relationship_by_hash(
                        h,
                        king_hash,
                        str(rel_type),
                        bidirectional=bidirectional
                    )
                return 0
        except Exception as exc:
            log(f"Failed to set store relationships: {exc}", file=sys.stderr)
            return 1

    if store_name and (not is_folder_store):
        # Hydrus store/hash mode
        if hydrus_client is None:
            log("Hydrus client unavailable for this store", file=sys.stderr)
            return 1

        # Verify hashes exist in this Hydrus backend to prevent cross-instance edges.
        if king_hash and (not _hydrus_hash_exists(hydrus_client, king_hash)):
            log(
                f"Cross-instance relationship blocked: king hash not found in store '{store_name}'",
                file=sys.stderr,
            )
            return 1

        # Mode 1: first is king
        if not king_hash:
            first_hash = None
            for item in items_to_process:
                h, item_store = _extract_hash_and_store(item)
                if item_store and store_name and str(item_store) != str(store_name):
                    log(
                        f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
                        file=sys.stderr,
                    )
                    return 1
                if not h:
                    continue
                if not first_hash:
                    first_hash = h
                    if not _hydrus_hash_exists(hydrus_client, first_hash):
                        log(
                            f"Cross-instance relationship blocked: hash not found in store '{store_name}'",
                            file=sys.stderr,
                        )
                        return 1
                    continue
                if h != first_hash:
                    if not _hydrus_hash_exists(hydrus_client, h):
                        log(
                            f"Cross-instance relationship blocked: hash not found in store '{store_name}'",
                            file=sys.stderr,
                        )
                        return 1
                    hydrus_client.set_relationship(h, first_hash, str(rel_type))
            return 0

        # Mode 2: explicit king
        for item in items_to_process:
            h, item_store = _extract_hash_and_store(item)
            if item_store and store_name and str(item_store) != str(store_name):
                log(
                    f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
                    file=sys.stderr,
                )
                return 1
            if not h or h == king_hash:
                continue
            if not _hydrus_hash_exists(hydrus_client, h):
                log(
                    f"Cross-instance relationship blocked: hash not found in store '{store_name}'",
                    file=sys.stderr,
                )
                return 1
            hydrus_client.set_relationship(h, king_hash, str(rel_type))
        return 0

    # Process each item in the list (legacy path-based mode)
    for item in items_to_process:
        # Extract hash and path from current item
        file_hash = None
        file_path_from_result = None

        if isinstance(item, dict):
            file_hash = item.get("hash_hex") or item.get("hash")
            file_path_from_result = item.get("file_path") or item.get(
                "path"
            ) or item.get("target")
        else:
            file_hash = getattr(item, "hash_hex", None) or getattr(item, "hash", None)
            file_path_from_result = getattr(item,
                                            "file_path",
                                            None) or getattr(item,
                                                             "path",
                                                             None)

        # Handle relationships for local-file results using the configured
        # local plugin root when available.
        from SYS.config import get_local_storage_path

        local_storage_path = get_local_storage_path(config) if config else None
        use_local_storage = bool(local_storage_path)
        local_storage_root: Optional[Path] = None
        if local_storage_path:
            try:
                local_storage_root = Path(local_storage_path)
            except Exception:
                local_storage_root = None

        if use_local_storage and file_path_from_result:
            try:
                file_path_obj = Path(str(file_path_from_result))
            except Exception as exc:
                log(f"Local library error: {exc}", file=sys.stderr)
                return 1

            if not file_path_obj.exists():
                # Not a local file; fall through to Hydrus if possible.
                file_path_obj = None

            if file_path_obj is not None:
                try:
                    if local_storage_root is None:
                        log("Local plugin path unavailable", file=sys.stderr)
                        return 1

                    with LocalLibrarySearchOptimizer(local_storage_root) as opt:
                        if opt.db is None:
                            log("Local library DB unavailable", file=sys.stderr)
                            return 1

                        if king_hash:
                            normalized_king = _normalize_hash_hex(str(king_hash))
                            if not normalized_king:
                                log(f"King hash invalid: {king_hash}", file=sys.stderr)
                                return 1
                            king_file_path = opt.db.search_hash(normalized_king)
                            if not king_file_path:
                                log(
                                    f"King hash not found in local DB: {king_hash}",
                                    file=sys.stderr
                                )
                                return 1

                            bidirectional = str(rel_type).lower() != "alt"
                            opt.db.set_relationship(
                                file_path_obj,
                                king_file_path,
                                rel_type,
                                bidirectional=bidirectional
                            )
                            log(
                                f"Set {rel_type} relationship: {file_path_obj.name} -> {king_file_path.name}",
                                file=sys.stderr,
                            )
                            _refresh_relationship_view_if_current(
                                None,
                                str(file_path_obj),
                                str(king_file_path),
                                config
                            )
                        else:
                            # Original behavior: first becomes king, rest become alts
                            try:
                                king_path = ctx.load_value("relationship_king_path")
                            except Exception:
                                king_path = None

                            if not king_path:
                                try:
                                    ctx.store_value(
                                        "relationship_king_path",
                                        str(file_path_obj)
                                    )
                                    log(
                                        f"Established king file: {file_path_obj.name}",
                                        file=sys.stderr,
                                    )
                                    continue
                                except Exception:
                                    pass

                            if king_path and king_path != str(file_path_obj):
                                bidirectional = str(rel_type).lower() != "alt"
                                opt.db.set_relationship(
                                    file_path_obj,
                                    Path(king_path),
                                    rel_type,
                                    bidirectional=bidirectional,
                                )
                                log(
                                    f"Set {rel_type} relationship: {file_path_obj.name} -> {Path(king_path).name}",
                                    file=sys.stderr,
                                )
                                _refresh_relationship_view_if_current(
                                    None,
                                    str(file_path_obj),
                                    str(king_path),
                                    config
                                )
                except Exception as exc:
                    log(f"Local storage error: {exc}", file=sys.stderr)
                    return 1
                continue

        # PIPELINE MODE with Hydrus: Track relationships using hash
        if file_hash and hydrus_client:
            file_hash = _normalize_hash_hex(
                str(file_hash) if file_hash is not None else None
            )
            if not file_hash:
                log("Invalid file hash format", file=sys.stderr)
                return 1

            # If explicit -king provided, use it
            if king_hash:
                try:
                    hydrus_client.set_relationship(file_hash, king_hash, rel_type)
                    log(
                        f"[add-relationship] Set {rel_type} relationship: {file_hash} <-> {king_hash}",
                        file=sys.stderr,
                    )
                    _refresh_relationship_view_if_current(
                        file_hash,
                        str(file_path_from_result)
                        if file_path_from_result is not None else None,
                        king_hash,
                        config,
                    )
                except Exception as exc:
                    log(f"Failed to set relationship: {exc}", file=sys.stderr)
                    return 1
            else:
                # Original behavior: no explicit king, first becomes king, rest become alts
                try:
                    existing_king = ctx.load_value("relationship_king")
                except Exception:
                    existing_king = None

                # If this is the first item, make it the king
                if not existing_king:
                    try:
                        ctx.store_value("relationship_king", file_hash)
                        log(f"Established king hash: {file_hash}", file=sys.stderr)
                        continue  # Move to next item
                    except Exception:
                        pass

                # If we already have a king and this is a different hash, link them
                if existing_king and existing_king != file_hash:
                    try:
                        hydrus_client.set_relationship(
                            file_hash,
                            existing_king,
                            rel_type
                        )
                        log(
                            f"[add-relationship] Set {rel_type} relationship: {file_hash} <-> {existing_king}",
                            file=sys.stderr,
                        )
                        _refresh_relationship_view_if_current(
                            file_hash,
                            (
                                str(file_path_from_result)
                                if file_path_from_result is not None else None
                            ),
                            existing_king,
                            config,
                        )
                    except Exception as exc:
                        log(f"Failed to set relationship: {exc}", file=sys.stderr)
                        return 1

        # If we get here, we didn't have a usable local path and Hydrus isn't available/usable.

    return 0

    # FILE MODE: Read relationships from sidecar (legacy mode - for -path arg only)
    log(
        "Note: Use piping mode for easier relationships. Example: 1,2,3 | add-relationship",
        file=sys.stderr,
    )

    # Resolve media path from -path arg or result target
    target = getattr(result, "target", None) or getattr(result, "path", None)
    media_path = (
        arg_path
        if arg_path is not None else Path(str(target)) if isinstance(target,
                                                                     str) else None
    )
    if media_path is None:
        log("Provide -path <file> or pipe a local file result", file=sys.stderr)
        return 1

    # Validate local file
    if str(media_path).lower().startswith(("http://", "https://")):
        log("This cmdlet requires a local file path, not a URL", file=sys.stderr)
        return 1
    if not media_path.exists() or not media_path.is_file():
        log(f"File not found: {media_path}", file=sys.stderr)
        return 1

    # Build Hydrus client
    hydrus_provider = get_plugin("hydrusnetwork", config)
    try:
        hydrus_client = hydrus_provider.get_client() if hydrus_provider is not None else None
    except Exception as exc:
        log(f"Hydrus client unavailable: {exc}", file=sys.stderr)
        return 1

    if hydrus_client is None:
        log("Hydrus client unavailable", file=sys.stderr)
        return 1

    # Read sidecar to find relationship tags
    sidecar_path = find_sidecar(media_path)
    if sidecar_path is None:
        log(f"No sidecar found for {media_path.name}", file=sys.stderr)
        return 1

    try:
        _, tags, _ = read_sidecar(sidecar_path)
    except Exception as exc:
        log(f"Failed to read sidecar: {exc}", file=sys.stderr)
        return 1

    # Find relationship tags (format: "relationship: hash(king)<HASH>,hash(alt)<HASH>,hash(related)<HASH>")
    relationship_tags = [
        t for t in tags if isinstance(t, str) and t.lower().startswith("relationship:")
    ]

    if not relationship_tags:
        log("No relationship tags found in sidecar", file=sys.stderr)
        return 0  # Not an error, just nothing to do

    # Get the file hash from result (should have been set by add-file)
    file_hash = getattr(result, "hash_hex", None)
    if not file_hash:
        log("File hash not available (run add-file first)", file=sys.stderr)
        return 1

    file_hash = _normalize_hash_hex(file_hash)
    if not file_hash:
        log("Invalid file hash format", file=sys.stderr)
        return 1

    # Parse relationships from tags and apply them
    success_count = 0
    error_count = 0

    for rel_tag in relationship_tags:
        try:
            # Parse: "relationship: hash(king)<HASH>,hash(alt)<HASH>,hash(related)<HASH>"
            rel_str = rel_tag.split(":", 1)[1].strip()  # Get part after "relationship:"

            # Parse relationships
            rels = _extract_relationships_from_tag(f"relationship: {rel_str}")

            # Set the relationships in Hydrus
            for rel_type, related_hashes in rels.items():
                if not related_hashes:
                    continue

                for related_hash in related_hashes:
                    # Don't set relationship between hash and itself
                    if file_hash == related_hash:
                        continue

                    try:
                        hydrus_client.set_relationship(
                            file_hash,
                            related_hash,
                            rel_type
                        )
                        log(
                            f"[add-relationship] Set {rel_type} relationship: "
                            f"{file_hash} <-> {related_hash}",
                            file=sys.stderr,
                        )
                        success_count += 1
                    except Exception as exc:
                        log(
                            f"Failed to set {rel_type} relationship: {exc}",
                            file=sys.stderr
                        )
                        error_count += 1

        except Exception as exc:
            log(f"Failed to parse relationship tag: {exc}", file=sys.stderr)
            error_count += 1

    if success_count > 0:
        log(
            f"Successfully set {success_count} relationship(s) for {media_path.name}",
            file=sys.stderr,
        )
        ctx.emit(
            f"add-relationship: {media_path.name} ({success_count} relationships set)"
        )
        return 0
    elif error_count == 0:
        log("No relationships to set", file=sys.stderr)
        return 0  # Success with nothing to do
    else:
        log(f"Failed with {error_count} error(s)", file=sys.stderr)
        return 1


# Register cmdlet (no legacy decorator)
CMDLET.exec = _run
CMDLET.alias = ["add-rel"]
CMDLET.register()
