from __future__ import annotations

"""Utilities for materializing knowledge resource file IDs into actual markdown content.

A field whose JSON Schema has {"type": "string", "format": "knowledge_resource_id"}
(or an array/object recursively containing such) will be replaced by the file content.

Resolution Strategy (initial version):
1. Resource ID is treated as a filename (without path traversal) under base dir.
2. Base directory is provided by env var KNOWLEDGE_RESOURCE_DIR; default ./knowledge_resources relative to CWD.
3. Only markdown files (.md / .markdown) are read; others return an empty string.
4. Security: reject IDs containing path separators or parent traversal tokens.
5. Graceful fallback: if file missing or unreadable -> empty string.

Future Extensions:
- Map ID via database (Prisma) lookup.
- Support other formats (pdf, txt) with conversion.
- Cache file contents with TTL.
"""

from typing import Any, Dict
import os
import re
from functools import lru_cache
import asyncio
try:
    from turbo_agent_store.services import get_resource_content as _cfg_get_content  # runtime-level integration
except Exception:
    _cfg_get_content = None

_ALLOWED_ID_PATTERN = re.compile(r"^[A-Za-z0-9_.\-]+$")
_DEFAULT_BASE_DIR = "knowledge_resources"

@lru_cache(maxsize=256)
def _read_markdown(path: str) -> str:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except Exception:
        return ""

def _resolve_base_dir() -> str:
    base = os.getenv("KNOWLEDGE_RESOURCE_DIR", _DEFAULT_BASE_DIR)
    if not os.path.isabs(base):
        base = os.path.abspath(base)
    os.makedirs(base, exist_ok=True)
    return base

async def _materialize_value_async(val: Any) -> Any:
    # Prefer config-backed resolution if available
    if _cfg_get_content and isinstance(val, str):
        # assume val is knowledge_id; owner_id not available in runtime context -> try without
        try:
            content = await _cfg_get_content(val, owner_id=os.getenv("DEFAULT_OWNER_ID", ""))
            if content is not None:
                return content
        except Exception:
            pass
    # Fallback to local file system resolution
    if not isinstance(val, str):
        return val
    if not _ALLOWED_ID_PATTERN.match(val):
        return ""  # disallow traversal / unexpected chars
    base = _resolve_base_dir()
    # infer markdown path candidates
    for ext in (".md", ".markdown"):
        candidate = os.path.join(base, val + ext)
        if os.path.isfile(candidate):
            return _read_markdown(candidate)
    # direct file id (if user already includes extension)
    candidate_direct = os.path.join(base, val)
    if os.path.isfile(candidate_direct) and os.path.splitext(candidate_direct)[1] in {".md", ".markdown"}:
        return _read_markdown(candidate_direct)
    return ""  # not found or unsupported

def materialize_knowledge_resources(schema: Dict[str, Any], data: Any) -> Any:
    """Recursively traverse data guided by schema, replacing file IDs with content.

    Args:
        schema: JSON schema dict for the current node.
        data: corresponding data node.
    Returns:
        New data with knowledge_resource_id formats replaced by file contents.
    """
    if not isinstance(schema, dict):
        return data
    fmt = schema.get("format")
    t = schema.get("type")
    if fmt == "knowledge_resource_id" and t == "string":
        # bridge to async resolution by running event loop briefly
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(_materialize_value_async(data))
    if t == "object" and isinstance(data, dict):
        props = schema.get("properties", {}) or {}
        result = dict(data)
        for k, subschema in props.items():
            if k in result:
                result[k] = materialize_knowledge_resources(subschema, result[k])
        return result
    if t == "array" and isinstance(data, list):
        items_schema = schema.get("items", {}) or {}
        return [materialize_knowledge_resources(items_schema, v) for v in data]
    return data
