#!/usr/bin/env python3
"""
Novyx Core - Graph Helper Module
=================================
Graph analysis utilities for artifact relationship management.

Functions:
- scan_artifacts: Parse JSON-LD files in a directory
- get_primary_ids: Extract primary identifiers from an artifact
- extract_internal_links: Find internal references (urn:uuid:, novyx:)
- build_index: Create comprehensive graph index
"""

import json
from pathlib import Path
from typing import Dict, List, Set, Tuple


def scan_artifacts(directory: Path) -> Tuple[List[Dict], List[str]]:
    """
    Scan directory for JSON-LD artifacts.
    
    Args:
        directory: Path to scan recursively for .jsonld files
    
    Returns:
        Tuple of (artifacts list, errors list)
        Each artifact dict has: 'data' (parsed JSON), 'path' (relative Path)
    
    Example:
        >>> artifacts, errors = scan_artifacts(Path("memory"))
        >>> len(artifacts)
        6
        >>> len(errors)
        0
    """
    artifacts = []
    errors = []
    
    if not directory.exists():
        errors.append(f"Directory not found: {directory}")
        return artifacts, errors
    
    for jsonld_file in directory.rglob("*.jsonld"):
        try:
            with open(jsonld_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            artifacts.append({
                'data': data,
                'path': jsonld_file
            })
        except json.JSONDecodeError as e:
            errors.append(f"JSON parse error in {jsonld_file}: {e}")
        except Exception as e:
            errors.append(f"Failed to read {jsonld_file}: {e}")
    
    return artifacts, errors


def get_primary_ids(artifact: dict) -> Set[str]:
    """
    Extract primary identifiers from an artifact.
    
    Supports both legacy (v1.0) and new (v1.1.0) schemas:
    - uuid (v1.1.0)
    - Persistent_ID (v1.0)
    - @id (if it starts with "urn:uuid:")
    
    Args:
        artifact: Parsed JSON-LD artifact data
    
    Returns:
        Set of primary identifier strings
    
    Example:
        >>> artifact = {"uuid": "urn:uuid:abc123", "Persistent_ID": "urn:uuid:abc123"}
        >>> get_primary_ids(artifact)
        {'urn:uuid:abc123'}
    """
    ids = set()
    
    # v1.1.0: uuid field
    if 'uuid' in artifact and isinstance(artifact['uuid'], str):
        ids.add(artifact['uuid'])
    
    # v1.0: Persistent_ID field
    if 'Persistent_ID' in artifact and isinstance(artifact['Persistent_ID'], str):
        ids.add(artifact['Persistent_ID'])
    
    # Generic: @id field (if it's a urn:uuid)
    if '@id' in artifact and isinstance(artifact['@id'], str):
        if artifact['@id'].startswith('urn:uuid:'):
            ids.add(artifact['@id'])
    
    return ids


def extract_internal_links(artifact: dict) -> Set[str]:
    """
    Extract internal links from an artifact.
    
    Internal links are:
    - Strings starting with "urn:uuid:" (artifact references)
    - Strings starting with "novyx:" (namespace references)
    
    Searches:
    - Known link fields: Context_Link, novyx:semanticLinks, Depends_On
    - Top-level string values (shallow scan)
    
    Excludes:
    - schema.org URLs
    - @context URLs
    - External http(s) URLs
    
    Args:
        artifact: Parsed JSON-LD artifact data
    
    Returns:
        Set of internal link strings
    
    Example:
        >>> artifact = {
        ...     "Context_Link": "urn:uuid:abc123",
        ...     "novyx:semanticLinks": [{"targetId": "urn:uuid:def456"}]
        ... }
        >>> extract_internal_links(artifact)
        {'urn:uuid:abc123', 'urn:uuid:def456'}
    """
    links = set()
    
    # Known link fields
    # v1.0: Context_Link
    if 'Context_Link' in artifact and isinstance(artifact['Context_Link'], str):
        link = artifact['Context_Link']
        if link.startswith('urn:uuid:') or link.startswith('novyx:'):
            links.add(link)
    
    # v1.1.0: novyx:semanticLinks (array of objects with targetId)
    if 'novyx:semanticLinks' in artifact and isinstance(artifact['novyx:semanticLinks'], list):
        for link_obj in artifact['novyx:semanticLinks']:
            if isinstance(link_obj, dict) and 'targetId' in link_obj:
                target = link_obj['targetId']
                if isinstance(target, str) and (target.startswith('urn:uuid:') or target.startswith('novyx:')):
                    links.add(target)
    
    # Dependencies
    if 'Depends_On' in artifact:
        deps = artifact['Depends_On']
        if isinstance(deps, str):
            if deps.startswith('urn:uuid:') or deps.startswith('novyx:'):
                links.add(deps)
        elif isinstance(deps, list):
            for dep in deps:
                if isinstance(dep, str) and (dep.startswith('urn:uuid:') or dep.startswith('novyx:')):
                    links.add(dep)
    
    # Legacy: linkedHealthLogs, linkedResearch
    for field in ['linkedHealthLogs', 'linkedResearch']:
        if field in artifact:
            value = artifact[field]
            if isinstance(value, str):
                if value.startswith('urn:uuid:') or value.startswith('novyx:'):
                    links.add(value)
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, str) and (item.startswith('urn:uuid:') or item.startswith('novyx:')):
                        links.add(item)
            elif isinstance(value, dict) and '@id' in value:
                link = value['@id']
                if isinstance(link, str) and (link.startswith('urn:uuid:') or link.startswith('novyx:')):
                    links.add(link)
    
    # Shallow scan of top-level string values
    # Skip known non-link fields and @context
    skip_fields = {'@context', '@vocab', 'articleBody', 'content', 'name', 'title', 
                   'description', 'reasoning', 'context', 'expected_outcome',
                   'novyx:integrityHash', 'Integrity_Hash', 'novyx:ingestedAt',
                   'Temporal_Marker', 'createdAt', 'updatedAt'}
    
    for key, value in artifact.items():
        if key in skip_fields:
            continue
        
        if isinstance(value, str):
            # Check if it looks like an internal link
            if value.startswith('urn:uuid:') or value.startswith('novyx:'):
                # Exclude schema.org and common namespaces
                if not any(exclude in value for exclude in ['schema.org', 'http://', 'https://', '.ai/ns/', '.ai/vocab']):
                    links.add(value)
    
    return links


def build_index(artifacts: List[Dict]) -> Dict:
    """
    Build comprehensive graph index from artifacts.
    
    Args:
        artifacts: List of artifact dicts (from scan_artifacts)
    
    Returns:
        Dict with keys:
        - id_to_file: Map of primary ID -> file path
        - file_to_ids: Map of file path -> set of primary IDs
        - file_to_links: Map of file path -> set of internal links referenced
        - inbound_links: Map of target ID -> list of (source_file, link_type) tuples
    
    Example:
        >>> artifacts, _ = scan_artifacts(Path("memory"))
        >>> index = build_index(artifacts)
        >>> len(index['id_to_file'])
        6
    """
    id_to_file = {}
    file_to_ids = {}
    file_to_links = {}
    inbound_links = {}
    
    for artifact in artifacts:
        data = artifact['data']
        path = artifact['path']
        
        # Extract primary IDs
        primary_ids = get_primary_ids(data)
        file_to_ids[str(path)] = primary_ids
        
        # Map each ID to this file
        for pid in primary_ids:
            id_to_file[pid] = str(path)
        
        # Extract internal links
        internal_links = extract_internal_links(data)
        file_to_links[str(path)] = internal_links
        
        # Build inbound link index
        for link in internal_links:
            if link not in inbound_links:
                inbound_links[link] = []
            inbound_links[link].append(str(path))
    
    return {
        'id_to_file': id_to_file,
        'file_to_ids': file_to_ids,
        'file_to_links': file_to_links,
        'inbound_links': inbound_links
    }


def find_orphaned_links(index: Dict) -> Dict[str, Set[str]]:
    """
    Find orphaned internal links (referenced but no artifact has them as primary ID).
    
    Args:
        index: Graph index from build_index()
    
    Returns:
        Dict mapping source file -> set of orphaned link IDs
    
    Example:
        >>> index = build_index(artifacts)
        >>> orphaned = find_orphaned_links(index)
        >>> len(orphaned)
        0  # No orphans in a healthy graph
    """
    orphaned = {}
    known_ids = set(index['id_to_file'].keys())
    
    for file_path, links in index['file_to_links'].items():
        orphaned_in_file = set()
        for link in links:
            # Only check urn:uuid links (not novyx: namespace references)
            if link.startswith('urn:uuid:') and link not in known_ids:
                orphaned_in_file.add(link)
        
        if orphaned_in_file:
            orphaned[file_path] = orphaned_in_file
    
    return orphaned


if __name__ == "__main__":
    # CLI for testing graph functions
    import sys
    
    if len(sys.argv) < 2:
        print("Usage: python3 -m tools.graph <directory>")
        sys.exit(1)
    
    directory = Path(sys.argv[1])
    
    print(f"Scanning {directory}...")
    artifacts, errors = scan_artifacts(directory)
    
    if errors:
        print(f"\n⚠️  Errors ({len(errors)}):")
        for error in errors:
            print(f"  {error}")
    
    print(f"\n📊 Results:")
    print(f"  Artifacts: {len(artifacts)}")
    
    index = build_index(artifacts)
    print(f"  Primary IDs: {len(index['id_to_file'])}")
    print(f"  Total internal links: {sum(len(links) for links in index['file_to_links'].values())}")
    
    orphaned = find_orphaned_links(index)
    if orphaned:
        print(f"\n❌ Orphaned links found in {len(orphaned)} files:")
        for file_path, links in orphaned.items():
            print(f"  {file_path}: {len(links)} orphans")
    else:
        print(f"\n✅ No orphaned links")
