#!/usr/bin/env python3
"""
Novyx Core - Export Module (Phase 11)
======================================
Export artifacts to external formats for migrations and integrations.

Supported Formats:
- RDF/Turtle: Standard semantic web format using rdflib
- Neo4j: Graph database format with Cypher statements
- JSON-LD: Native format (pass-through)

Features:
- CORE_SCHEMA property mappings
- Multi-tenant aware exports
- Streaming export for large datasets
- Integrity hash preservation

Usage:
    from tools.export import export_artifacts, ExportFormat

    # Export to RDF/Turtle
    export_artifacts(Path("memory"), ExportFormat.RDF, Path("export.ttl"))

    # Export to Neo4j Cypher
    export_artifacts(Path("memory"), ExportFormat.NEO4J, Path("export.cypher"))
"""

import argparse
import json
import sys
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Dict, List, Any, Optional, Generator, Tuple
from urllib.parse import quote

# Optional dependency handling
try:
    from rdflib import Graph, Namespace, URIRef, Literal, BNode
    from rdflib.namespace import RDF, RDFS, XSD, DCTERMS
    RDFLIB_AVAILABLE = True
except ImportError:
    RDFLIB_AVAILABLE = False
    # Define fallback for namespace URIs
    DCTERMS = type('DCTERMS', (), {'__str__': lambda s: "http://purl.org/dc/terms/"})()

try:
    from neo4j import GraphDatabase
    NEO4J_AVAILABLE = True
except ImportError:
    NEO4J_AVAILABLE = False


class ExportFormat(Enum):
    """Supported export formats."""
    RDF = "rdf"
    NEO4J = "neo4j"
    JSONLD = "jsonld"
    CYPHER = "cypher"  # Alias for NEO4J


# Namespace definitions for RDF export
NOVYX_NS = "https://novyx.ai/vocab#"
SCHEMA_NS = "https://schema.org/"
DCTERMS_NS = "http://purl.org/dc/terms/"

# CORE_SCHEMA property mappings to RDF predicates
SCHEMA_PROPERTY_MAP = {
    # Core identifiers
    "uuid": f"{NOVYX_NS}uuid",
    "Persistent_ID": f"{NOVYX_NS}persistentIdentifier",
    "@id": f"{NOVYX_NS}id",
    "id": f"{NOVYX_NS}id",

    # Timestamps
    "createdAt": f"{DCTERMS_NS}created",
    "updatedAt": f"{DCTERMS_NS}modified",
    "Temporal_Marker": f"{NOVYX_NS}temporalMarker",
    "novyx:ingestedAt": f"{NOVYX_NS}ingestedAt",
    "novyx:updatedAt": f"{NOVYX_NS}updatedAt",

    # Integrity
    "novyx:integrityHash": f"{NOVYX_NS}integrityHash",
    "Integrity_Hash": f"{NOVYX_NS}integrityHash",

    # Content
    "name": f"{SCHEMA_NS}name",
    "title": f"{SCHEMA_NS}name",
    "articleBody": f"{SCHEMA_NS}articleBody",
    "content": f"{SCHEMA_NS}text",
    "description": f"{SCHEMA_NS}description",

    # Decision-specific
    "context": f"{NOVYX_NS}context",
    "options_considered": f"{NOVYX_NS}optionsConsidered",
    "chosen_option": f"{NOVYX_NS}chosenOption",
    "reasoning": f"{NOVYX_NS}reasoning",
    "constraints": f"{NOVYX_NS}constraints",
    "assumptions": f"{NOVYX_NS}assumptions",
    "confidence": f"{NOVYX_NS}confidence",
    "expected_outcome": f"{NOVYX_NS}expectedOutcome",
    "category": f"{SCHEMA_NS}category",

    # API Endpoint-specific
    "method": f"{NOVYX_NS}httpMethod",
    "path": f"{NOVYX_NS}apiPath",
    "parameters": f"{NOVYX_NS}parameters",
    "response_schema": f"{NOVYX_NS}responseSchema",
    "authentication": f"{NOVYX_NS}authentication",
    "rate_limit": f"{NOVYX_NS}rateLimit",

    # GraphQL-specific
    "operation_type": f"{NOVYX_NS}operationType",
    "operation_name": f"{NOVYX_NS}operationName",
    "arguments": f"{NOVYX_NS}arguments",
    "return_type": f"{NOVYX_NS}returnType",
    "resolver": f"{NOVYX_NS}resolver",

    # Tenant-specific
    "tenant_id": f"{NOVYX_NS}tenantId",
    "novyx:tenantId": f"{NOVYX_NS}tenantId",
    "admin_email": f"{NOVYX_NS}adminEmail",
    "plan_type": f"{NOVYX_NS}planType",
    "status": f"{NOVYX_NS}status",
    "max_artifacts": f"{NOVYX_NS}maxArtifacts",
    "features": f"{NOVYX_NS}features",
    "users": f"{NOVYX_NS}users",

    # Version-specific
    "artifact_id": f"{NOVYX_NS}artifactId",
    "version_number": f"{NOVYX_NS}versionNumber",
    "parent_version": f"{NOVYX_NS}parentVersion",
    "diff": f"{NOVYX_NS}diff",
    "change_summary": f"{NOVYX_NS}changeSummary",
    "changed_by": f"{NOVYX_NS}changedBy",
    "change_type": f"{NOVYX_NS}changeType",

    # Links
    "sameAs": f"{SCHEMA_NS}sameAs",
    "novyx:semanticLinks": f"{NOVYX_NS}semanticLinks",
}

# Type mappings for RDF
TYPE_MAP = {
    "Decision": f"{NOVYX_NS}Decision",
    "DigitalDocument": f"{SCHEMA_NS}DigitalDocument",
    "APIEndpoint": f"{NOVYX_NS}APIEndpoint",
    "GraphQLOperation": f"{NOVYX_NS}GraphQLOperation",
    "Tenant": f"{NOVYX_NS}Tenant",
    "Version": f"{NOVYX_NS}Version",
}

# Neo4j label mappings
NEO4J_LABEL_MAP = {
    "Decision": "Decision",
    "DigitalDocument": "Document",
    "APIEndpoint": "APIEndpoint",
    "GraphQLOperation": "GraphQLOperation",
    "Tenant": "Tenant",
    "Version": "Version",
}

# Properties to skip in exports (internal/computed)
SKIP_PROPERTIES = {
    "@context",
    "novyx:embedding",  # Large vectors, skip by default
    "embedding",
}


def stream_artifacts(
    directory: Path,
    *,
    tenant_id: Optional[str] = None
) -> Generator[Tuple[Dict[str, Any], Path], None, None]:
    """
    Stream artifacts from directory for export.

    Args:
        directory: Directory containing .jsonld files
        tenant_id: Optional tenant filter

    Yields:
        Tuple of (artifact_dict, file_path)
    """
    scan_dir = directory / tenant_id if tenant_id else directory

    if not scan_dir.exists():
        return

    for jsonld_path in scan_dir.rglob("*.jsonld"):
        try:
            with open(jsonld_path, 'r', encoding='utf-8') as f:
                artifact = json.load(f)
                yield artifact, jsonld_path
        except (json.JSONDecodeError, IOError):
            continue


def get_artifact_id(artifact: Dict[str, Any]) -> str:
    """Extract the primary ID from an artifact."""
    return (
        artifact.get("uuid") or
        artifact.get("@id") or
        artifact.get("Persistent_ID") or
        artifact.get("id") or
        f"urn:uuid:{hash(json.dumps(artifact, sort_keys=True))}"
    )


def get_artifact_types(artifact: Dict[str, Any]) -> List[str]:
    """Extract types from an artifact."""
    type_val = artifact.get("@type", [])
    if isinstance(type_val, str):
        return [type_val]
    return type_val if isinstance(type_val, list) else []


def export_to_rdf(
    directory: Path,
    output_path: Path,
    *,
    tenant_id: Optional[str] = None,
    include_embeddings: bool = False,
    format: str = "turtle"
) -> Dict[str, Any]:
    """
    Export artifacts to RDF format.

    Args:
        directory: Source directory
        output_path: Output file path
        tenant_id: Optional tenant filter
        include_embeddings: Include vector embeddings (default: False)
        format: RDF serialization format (turtle, xml, n3, nt)

    Returns:
        Export statistics dict
    """
    if not RDFLIB_AVAILABLE:
        raise ImportError("rdflib not available. Install with: pip install rdflib")

    g = Graph()

    # Bind namespaces
    NOVYX = Namespace(NOVYX_NS)
    SCHEMA = Namespace(SCHEMA_NS)
    g.bind("novyx", NOVYX)
    g.bind("schema", SCHEMA)
    g.bind("dcterms", DCTERMS)

    stats = {
        "total_artifacts": 0,
        "exported": 0,
        "failed": 0,
        "triples": 0,
    }

    skip_props = SKIP_PROPERTIES.copy()
    if not include_embeddings:
        skip_props.add("novyx:embedding")
        skip_props.add("embedding")

    for artifact, path in stream_artifacts(directory, tenant_id=tenant_id):
        stats["total_artifacts"] += 1

        try:
            artifact_id = get_artifact_id(artifact)
            subject = URIRef(artifact_id) if artifact_id.startswith("urn:") else URIRef(f"urn:uuid:{artifact_id}")

            # Add types
            for type_name in get_artifact_types(artifact):
                type_uri = TYPE_MAP.get(type_name, f"{NOVYX_NS}{type_name}")
                g.add((subject, RDF.type, URIRef(type_uri)))
                stats["triples"] += 1

            # Add properties
            for key, value in artifact.items():
                if key in skip_props or key.startswith("@"):
                    continue

                predicate_uri = SCHEMA_PROPERTY_MAP.get(key, f"{NOVYX_NS}{key}")
                predicate = URIRef(predicate_uri)

                # Handle different value types
                if isinstance(value, str):
                    g.add((subject, predicate, Literal(value)))
                    stats["triples"] += 1
                elif isinstance(value, bool):
                    g.add((subject, predicate, Literal(value, datatype=XSD.boolean)))
                    stats["triples"] += 1
                elif isinstance(value, int):
                    g.add((subject, predicate, Literal(value, datatype=XSD.integer)))
                    stats["triples"] += 1
                elif isinstance(value, float):
                    g.add((subject, predicate, Literal(value, datatype=XSD.decimal)))
                    stats["triples"] += 1
                elif isinstance(value, list):
                    for item in value:
                        if isinstance(item, str):
                            # Check if it's a URI
                            if item.startswith("http") or item.startswith("urn:"):
                                g.add((subject, predicate, URIRef(item)))
                            else:
                                g.add((subject, predicate, Literal(item)))
                            stats["triples"] += 1
                        elif isinstance(item, dict):
                            # Create blank node for complex items
                            bnode = BNode()
                            g.add((subject, predicate, bnode))
                            stats["triples"] += 1
                            for k, v in item.items():
                                if isinstance(v, str):
                                    g.add((bnode, URIRef(f"{NOVYX_NS}{k}"), Literal(v)))
                                    stats["triples"] += 1

            stats["exported"] += 1

        except Exception as e:
            stats["failed"] += 1
            print(f"Warning: Failed to export {path}: {e}", file=sys.stderr)

    # Serialize to file
    output_path.parent.mkdir(parents=True, exist_ok=True)
    g.serialize(destination=str(output_path), format=format)

    stats["output_path"] = str(output_path)
    stats["format"] = format

    return stats


def export_to_neo4j_cypher(
    directory: Path,
    output_path: Path,
    *,
    tenant_id: Optional[str] = None,
    include_embeddings: bool = False
) -> Dict[str, Any]:
    """
    Export artifacts to Neo4j Cypher statements.

    Args:
        directory: Source directory
        output_path: Output file path (.cypher)
        tenant_id: Optional tenant filter
        include_embeddings: Include vector embeddings

    Returns:
        Export statistics dict
    """
    stats = {
        "total_artifacts": 0,
        "exported": 0,
        "failed": 0,
        "nodes": 0,
        "relationships": 0,
    }

    skip_props = SKIP_PROPERTIES.copy()
    if not include_embeddings:
        skip_props.add("novyx:embedding")
        skip_props.add("embedding")

    output_path.parent.mkdir(parents=True, exist_ok=True)

    with open(output_path, 'w', encoding='utf-8') as f:
        # Write header
        f.write("// Novyx Core Export to Neo4j\n")
        f.write(f"// Generated: {datetime.utcnow().isoformat()}Z\n")
        f.write(f"// Source: {directory}\n")
        if tenant_id:
            f.write(f"// Tenant: {tenant_id}\n")
        f.write("\n// Create constraints (run once)\n")
        f.write("CREATE CONSTRAINT IF NOT EXISTS FOR (n:Artifact) REQUIRE n.uuid IS UNIQUE;\n")
        f.write("CREATE CONSTRAINT IF NOT EXISTS FOR (n:Decision) REQUIRE n.uuid IS UNIQUE;\n")
        f.write("CREATE CONSTRAINT IF NOT EXISTS FOR (n:Document) REQUIRE n.uuid IS UNIQUE;\n")
        f.write("\n// Nodes\n")

        relationships = []

        for artifact, path in stream_artifacts(directory, tenant_id=tenant_id):
            stats["total_artifacts"] += 1

            try:
                artifact_id = get_artifact_id(artifact)
                types = get_artifact_types(artifact)

                # Determine Neo4j label
                labels = ["Artifact"]
                for t in types:
                    if t in NEO4J_LABEL_MAP:
                        labels.append(NEO4J_LABEL_MAP[t])

                label_str = ":".join(labels)

                # Build properties
                props = {}
                for key, value in artifact.items():
                    if key in skip_props or key.startswith("@"):
                        continue

                    # Normalize property name for Neo4j (camelCase)
                    prop_name = key.replace("novyx:", "").replace(":", "_")

                    if isinstance(value, (str, int, float, bool)):
                        props[prop_name] = value
                    elif isinstance(value, list):
                        # Track relationships
                        if key in ("sameAs", "novyx:semanticLinks"):
                            for link in value:
                                if isinstance(link, str) and (link.startswith("http") or link.startswith("urn:")):
                                    relationships.append((artifact_id, "SAME_AS", link))
                        else:
                            # Store arrays as JSON strings
                            props[prop_name] = json.dumps(value)

                # Ensure uuid is set
                props["uuid"] = artifact_id

                # Generate CREATE statement
                props_str = ", ".join(
                    f"{k}: {_cypher_value(v)}" for k, v in props.items()
                )
                f.write(f"CREATE (:{label_str} {{{props_str}}});\n")
                stats["nodes"] += 1
                stats["exported"] += 1

            except Exception as e:
                stats["failed"] += 1
                print(f"Warning: Failed to export {path}: {e}", file=sys.stderr)

        # Write relationships
        if relationships:
            f.write("\n// Relationships\n")
            for src, rel_type, target in relationships:
                f.write(
                    f"MATCH (a:Artifact {{uuid: {_cypher_value(src)}}}), "
                    f"(b:Artifact {{uuid: {_cypher_value(target)}}}) "
                    f"CREATE (a)-[:{rel_type}]->(b);\n"
                )
                stats["relationships"] += 1

    stats["output_path"] = str(output_path)

    return stats


def _cypher_value(value: Any) -> str:
    """Convert Python value to Cypher literal."""
    if value is None:
        return "null"
    elif isinstance(value, bool):
        return "true" if value else "false"
    elif isinstance(value, str):
        escaped = value.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n")
        return f"'{escaped}'"
    elif isinstance(value, (int, float)):
        return str(value)
    else:
        escaped = json.dumps(value).replace("'", "\\'")
        return f"'{escaped}'"


def export_to_neo4j_driver(
    directory: Path,
    uri: str,
    username: str,
    password: str,
    *,
    tenant_id: Optional[str] = None,
    database: str = "neo4j",
    batch_size: int = 100
) -> Dict[str, Any]:
    """
    Export artifacts directly to Neo4j using the driver.

    Args:
        directory: Source directory
        uri: Neo4j connection URI (e.g., bolt://localhost:7687)
        username: Neo4j username
        password: Neo4j password
        tenant_id: Optional tenant filter
        database: Neo4j database name
        batch_size: Batch size for transactions

    Returns:
        Export statistics dict
    """
    if not NEO4J_AVAILABLE:
        raise ImportError("neo4j driver not available. Install with: pip install neo4j")

    stats = {
        "total_artifacts": 0,
        "exported": 0,
        "failed": 0,
        "nodes": 0,
        "relationships": 0,
    }

    driver = GraphDatabase.driver(uri, auth=(username, password))

    try:
        with driver.session(database=database) as session:
            # Create constraints
            session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (n:Artifact) REQUIRE n.uuid IS UNIQUE")

            batch = []

            for artifact, path in stream_artifacts(directory, tenant_id=tenant_id):
                stats["total_artifacts"] += 1

                try:
                    artifact_id = get_artifact_id(artifact)
                    types = get_artifact_types(artifact)

                    # Build labels
                    labels = ["Artifact"]
                    for t in types:
                        if t in NEO4J_LABEL_MAP:
                            labels.append(NEO4J_LABEL_MAP[t])

                    # Build properties
                    props = {"uuid": artifact_id}
                    for key, value in artifact.items():
                        if key in SKIP_PROPERTIES or key.startswith("@"):
                            continue
                        prop_name = key.replace("novyx:", "").replace(":", "_")
                        if isinstance(value, (str, int, float, bool)):
                            props[prop_name] = value
                        elif isinstance(value, list):
                            props[prop_name] = json.dumps(value)

                    batch.append({
                        "labels": labels,
                        "props": props
                    })

                    if len(batch) >= batch_size:
                        _execute_batch(session, batch)
                        stats["nodes"] += len(batch)
                        stats["exported"] += len(batch)
                        batch = []

                except Exception as e:
                    stats["failed"] += 1
                    print(f"Warning: Failed to export {path}: {e}", file=sys.stderr)

            # Final batch
            if batch:
                _execute_batch(session, batch)
                stats["nodes"] += len(batch)
                stats["exported"] += len(batch)

    finally:
        driver.close()

    return stats


def _execute_batch(session, batch: List[Dict]) -> None:
    """Execute a batch of node creations."""
    for item in batch:
        labels_str = ":".join(item["labels"])
        session.run(
            f"MERGE (n:{labels_str} {{uuid: $uuid}}) SET n += $props",
            uuid=item["props"]["uuid"],
            props=item["props"]
        )


def export_artifacts(
    directory: Path,
    format: ExportFormat,
    output_path: Path,
    *,
    tenant_id: Optional[str] = None,
    include_embeddings: bool = False,
    **kwargs
) -> Dict[str, Any]:
    """
    Export artifacts to specified format.

    Args:
        directory: Source directory containing artifacts
        format: Export format (RDF, NEO4J, JSONLD)
        output_path: Output file path
        tenant_id: Optional tenant filter
        include_embeddings: Include vector embeddings (default: False)
        **kwargs: Format-specific options

    Returns:
        Export statistics dict
    """
    if format == ExportFormat.RDF:
        return export_to_rdf(
            directory, output_path,
            tenant_id=tenant_id,
            include_embeddings=include_embeddings,
            format=kwargs.get("rdf_format", "turtle")
        )
    elif format in (ExportFormat.NEO4J, ExportFormat.CYPHER):
        return export_to_neo4j_cypher(
            directory, output_path,
            tenant_id=tenant_id,
            include_embeddings=include_embeddings
        )
    elif format == ExportFormat.JSONLD:
        # Pass-through: copy artifacts to single file
        return _export_to_jsonld(
            directory, output_path,
            tenant_id=tenant_id,
            include_embeddings=include_embeddings
        )
    else:
        raise ValueError(f"Unsupported export format: {format}")


def _export_to_jsonld(
    directory: Path,
    output_path: Path,
    *,
    tenant_id: Optional[str] = None,
    include_embeddings: bool = False
) -> Dict[str, Any]:
    """Export artifacts as consolidated JSON-LD."""
    stats = {
        "total_artifacts": 0,
        "exported": 0,
        "failed": 0,
    }

    skip_props = set()
    if not include_embeddings:
        skip_props.add("novyx:embedding")
        skip_props.add("embedding")

    artifacts = []

    for artifact, path in stream_artifacts(directory, tenant_id=tenant_id):
        stats["total_artifacts"] += 1
        try:
            if skip_props:
                artifact = {k: v for k, v in artifact.items() if k not in skip_props}
            artifacts.append(artifact)
            stats["exported"] += 1
        except Exception:
            stats["failed"] += 1

    output_path.parent.mkdir(parents=True, exist_ok=True)

    export_doc = {
        "@context": {
            "@vocab": "https://schema.org/",
            "novyx": "https://novyx.ai/ns/core#"
        },
        "@graph": artifacts,
        "novyx:exportedAt": datetime.utcnow().isoformat() + "Z",
        "novyx:sourceDirectory": str(directory),
        "novyx:artifactCount": len(artifacts)
    }

    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(export_doc, f, indent=2)

    stats["output_path"] = str(output_path)

    return stats


def check_dependencies(format: ExportFormat) -> Tuple[bool, str]:
    """Check if dependencies for export format are available."""
    if format == ExportFormat.RDF:
        if not RDFLIB_AVAILABLE:
            return False, "rdflib not available. Install with: pip install rdflib"
    elif format in (ExportFormat.NEO4J, ExportFormat.CYPHER):
        pass  # Cypher export doesn't require driver

    return True, ""


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Novyx Core Export - Phase 11"
    )
    parser.add_argument(
        "--directory", "-d",
        default="memory",
        help="Source directory (default: memory)"
    )
    parser.add_argument(
        "--format", "-f",
        choices=["rdf", "neo4j", "cypher", "jsonld"],
        default="rdf",
        help="Export format (default: rdf)"
    )
    parser.add_argument(
        "--output", "-o",
        required=True,
        help="Output file path"
    )
    parser.add_argument(
        "--tenant",
        help="Filter by tenant ID"
    )
    parser.add_argument(
        "--include-embeddings",
        action="store_true",
        help="Include vector embeddings in export"
    )
    parser.add_argument(
        "--rdf-format",
        choices=["turtle", "xml", "n3", "nt"],
        default="turtle",
        help="RDF serialization format (default: turtle)"
    )

    args = parser.parse_args()

    # Map format string to enum
    format_map = {
        "rdf": ExportFormat.RDF,
        "neo4j": ExportFormat.NEO4J,
        "cypher": ExportFormat.CYPHER,
        "jsonld": ExportFormat.JSONLD,
    }
    export_format = format_map[args.format]

    # Check dependencies
    available, msg = check_dependencies(export_format)
    if not available:
        print(f"Error: {msg}", file=sys.stderr)
        sys.exit(1)

    print(f"Exporting from {args.directory} to {args.format.upper()}...")

    stats = export_artifacts(
        Path(args.directory),
        export_format,
        Path(args.output),
        tenant_id=args.tenant,
        include_embeddings=args.include_embeddings,
        rdf_format=args.rdf_format
    )

    print(f"\nExport Complete:")
    print(f"  Total artifacts: {stats['total_artifacts']}")
    print(f"  Exported: {stats['exported']}")
    print(f"  Failed: {stats['failed']}")
    if 'triples' in stats:
        print(f"  RDF triples: {stats['triples']}")
    if 'nodes' in stats:
        print(f"  Neo4j nodes: {stats['nodes']}")
    print(f"  Output: {stats['output_path']}")
