#!/usr/bin/env python3
"""
Script to generate a JSON schema for the langgraph-cli Config class.

This script creates a schema.json file that can be referenced in langgraph.json files
to provide IDE autocompletion and validation.
"""

import inspect
import json
import textwrap
from pathlib import Path

import msgspec

from langgraph_cli.schemas import (
    AuthConfig,
    CacheConfig,
    CheckpointerConfig,
    Config,
    ConfigurableHeaderConfig,
    CorsConfig,
    GraphDef,
    HttpConfig,
    IndexConfig,
    SecurityConfig,
    SerdeConfig,
    StoreConfig,
    ThreadTTLConfig,
    TTLConfig,
    WebhooksConfig,
    WebhookUrlPolicy,
)


def add_descriptions_to_schema(schema, cls):
    """Add docstring descriptions to the schema properties."""
    if schema.get("description"):
        schema["description"] = inspect.cleandoc(schema["description"])
    elif class_doc := inspect.getdoc(cls):
        schema["description"] = inspect.cleandoc(class_doc)
    # Get attribute docstrings from the class
    attr_docs = {}

    # Also check class annotations for docstrings
    source_lines = inspect.getsourcelines(cls)[0]
    current_attr = None
    docstring_lines = []

    for line in source_lines:
        line = line.strip()

        # Check for attribute definition (TypedDict style)
        if ":" in line and not line.startswith("#") and not line.startswith('"""'):
            parts = line.split(":", 1)
            if len(parts) == 2 and parts[0].strip().isidentifier():
                # If we were collecting a docstring, save it for the previous attribute
                if current_attr and docstring_lines:
                    attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
                    docstring_lines = []

                current_attr = parts[0].strip()

        # Check for docstring after attribute
        elif line.startswith('"""') and current_attr:
            # Start or end of a docstring
            if len(line) > 3 and line.endswith('"""'):
                # Single line docstring
                attr_docs[current_attr] = line.strip('"')
                current_attr = None
            elif docstring_lines:
                # End of multi-line docstring
                docstring_lines.append(line.rstrip('"'))
                attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
                docstring_lines = []
                current_attr = None
            else:
                # Start of multi-line docstring
                docstring_lines.append(line.lstrip('"'))

        # Continue multi-line docstring
        elif docstring_lines and current_attr:
            docstring_lines.append(line.strip('"'))

    # Add the last docstring if there is one
    if current_attr and docstring_lines:
        attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')

    # Add descriptions to properties
    if "properties" in schema:
        for prop_name, prop_schema in schema["properties"].items():
            # First try to get from attribute docstrings
            if prop_name in attr_docs and "description" not in prop_schema:
                prop_schema["description"] = textwrap.dedent(attr_docs[prop_name])
            # Fall back to class docstring parsing
            elif class_doc:
                for line in class_doc.split("\n"):
                    if line.strip().startswith(
                        f"{prop_name}:"
                    ) or line.strip().startswith(f'"{prop_name}"'):
                        description = line.split(":", 1)[1].strip()
                        if description and "description" not in prop_schema:
                            prop_schema["description"] = description
                            break

    # Recursively process nested definitions
    if "$defs" in schema:
        for def_name, def_schema in schema["$defs"].items():
            # Find the class that corresponds to this definition
            for potential_cls in [
                Config,
                GraphDef,
                StoreConfig,
                IndexConfig,
                AuthConfig,
                SecurityConfig,
                HttpConfig,
                CorsConfig,
                CacheConfig,
                ThreadTTLConfig,
                CheckpointerConfig,
                SerdeConfig,
                TTLConfig,
                ConfigurableHeaderConfig,
                WebhooksConfig,
                WebhookUrlPolicy,
            ]:
                if potential_cls.__name__ == def_name:
                    add_descriptions_to_schema(def_schema, potential_cls)
                    break

    return schema


def generate_schema():
    """Generate a JSON schema for the Config class using msgspec."""
    # Generate the basic schema
    schema = msgspec.json.schema(Config)

    # Add title and description
    schema["title"] = "LangGraph CLI Configuration"
    schema["description"] = "Configuration schema for langgraph-cli"

    # Add docstring descriptions
    schema = add_descriptions_to_schema(schema, Config)

    # Add constraint that only one of python_version or node_version should be specified
    config_schema = schema["$defs"]["Config"]

    # Create two subschemas: one with python_version and one with node_version
    # Define properties specific to Python projects
    python_specific_props = ["python_version", "pip_config_file"]
    # Define properties specific to Node.js projects
    node_specific_props = ["node_version"]
    # Define properties common to both project types
    common_props = [
        k
        for k in config_schema["properties"]
        if k not in python_specific_props and k not in node_specific_props
    ]

    # Create legacy Python schema with python_version and pip_config_file
    legacy_python_schema = {
        "type": "object",
        "properties": {
            # Include Python-specific properties
            **{k: config_schema["properties"][k].copy() for k in python_specific_props},
            # Include common properties
            **{k: config_schema["properties"][k].copy() for k in common_props},
        },
        "required": ["dependencies", "graphs"],
    }
    legacy_python_schema["properties"]["pip_installer"] = {
        "anyOf": [
            {"type": "string", "enum": ["auto", "pip", "uv"]},
            {"type": "null"},
        ]
    }

    uv_source_python_schema = {
        "type": "object",
        "properties": {
            **{
                k: config_schema["properties"][k].copy()
                for k in python_specific_props + common_props
            },
        },
        "required": ["graphs", "source"],
    }
    # source must be a UvSource object (not null)
    uv_source_python_schema["properties"]["source"] = {"$ref": "#/$defs/UvSource"}
    uv_source_python_schema["properties"]["pip_installer"] = {
        "anyOf": [
            {"type": "string", "enum": ["auto", "pip", "uv"]},
            {"type": "null"},
        ]
    }

    # Add enum constraint for python_version
    if "python_version" in legacy_python_schema["properties"]:
        legacy_python_schema["properties"]["python_version"]["enum"] = [
            "3.11",
            "3.12",
            "3.13",
        ]
    if "python_version" in uv_source_python_schema["properties"]:
        uv_source_python_schema["properties"]["python_version"]["enum"] = [
            "3.11",
            "3.12",
            "3.13",
        ]

    # Create Node.js schema with node_version
    node_schema = {
        "type": "object",
        "properties": {
            # Include Node-specific properties
            **{k: config_schema["properties"][k].copy() for k in node_specific_props},
            # Include common properties
            **{k: config_schema["properties"][k].copy() for k in common_props},
        },
        "required": ["node_version", "graphs"],
    }
    node_schema["properties"]["pip_installer"] = {
        "anyOf": [
            {"type": "string", "enum": ["auto", "pip", "uv"]},
            {"type": "null"},
        ]
    }

    # Add enum constraint for node_version
    if "node_version" in node_schema["properties"]:
        node_schema["properties"]["node_version"]["anyOf"] = [
            {"type": "string", "enum": ["20"]},
            {"type": "null"},
        ]

    # Add enum constraint for image_distro
    if "image_distro" in node_schema["properties"]:
        node_schema["properties"]["image_distro"]["anyOf"] = [
            {"type": "string", "enum": ["debian", "wolfi"]},
            {"type": "null"},
        ]

    # Replace the Config schema with a oneOf constraint
    config_schema["oneOf"] = [
        legacy_python_schema,
        uv_source_python_schema,
        node_schema,
    ]

    # Remove the properties field as it's now defined in the oneOf subschemas
    if "properties" in config_schema:
        del config_schema["properties"]

    return schema


def main():
    """Generate the schema and write it to a file."""
    schema = generate_schema()

    # Add versioning to the schema
    import importlib.metadata

    try:
        version = importlib.metadata.version("langgraph_cli").split(".")
        schema_version = f"v{version[0]}"
    except importlib.metadata.PackageNotFoundError:
        schema_version = "v1"

    # Add version to schema
    schema["version"] = schema_version

    config_dir = Path(__file__).parent / "schemas"

    # Create versioned schema file
    versioned_path = config_dir / f"schema.{schema_version}.json"
    with open(versioned_path, "w") as f:
        json.dump(schema, f, indent=2)

    # Also create a latest version
    latest_path = config_dir / "schema.json"
    with open(latest_path, "w") as f:
        json.dump(schema, f, indent=2)

    print(f"Schema written to {versioned_path} and {latest_path}")
    print(
        f"You can now add '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json'"
        f" or '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.{schema_version}.json'"
        " to your langgraph.json files"
    )


if __name__ == "__main__":
    main()
