import re
from collections.abc import Iterable
from pathlib import Path
from textwrap import dedent

import inflection
from graphql import GraphQLSchema
from graphql.language.ast import (
    DocumentNode,
)
from graphql.language.visitor import Visitor, visit

# Add graphql-core for robust parsing
try:
    from graphql import parse, visit
    from graphql.language.ast import DocumentNode

    HAVE_GRAPHQL_CORE = True
except Exception:
    HAVE_GRAPHQL_CORE = False

# Map GraphQL scalars -> Python type hints
SCALAR_MAP: dict[str, str] = {
    "ID": "str",
    "String": "str",
    "Boolean": "bool",
    "Int": "int",
    "Float": "float",
    "UUID": "str",  # example: map custom scalars here
    "FileMetadataIdType": "str",  # your schema’s scalar
    # add others (e.g., DateTime -> datetime.datetime) as needed
}


def _load_fragment_text(name: str, roots: Iterable[Path]) -> str:
    # 1) Try <Name>.graphql
    for root in roots:
        p = root / f"{name}.graphql"
        if p.exists():
            return p.read_text(encoding="utf-8")
    # 2) Fallback: scan *.graphql and match the fragment name
    for root in roots:
        for f in root.rglob("*.graphql"):
            txt = f.read_text(encoding="utf-8")
            if re.search(rf"fragment\s+{re.escape(name)}\s+on\b", txt):
                return txt
    raise FileNotFoundError(f"Fragment {name!r} not found under: {', '.join(map(str, roots))}")


def _parse_defs_and_spreads(doc: str):
    """Return (defined_fragment_names, referenced_fragment_names)."""
    if HAVE_GRAPHQL_CORE:
        ast: DocumentNode = parse(doc)

        class _FragVisitor(Visitor):
            def __init__(self):
                super().__init__()  # <-- IMPORTANT
                self.defined: set[str] = set()
                self.spreads: set[str] = set()

            # fragment Foo on Type { ... }
            def enter_fragment_definition(self, node, *_):
                self.defined.add(node.name.value)

            # ... Foo
            def enter_fragment_spread(self, node, *_):
                self.spreads.add(node.name.value)

        v = _FragVisitor()
        visit(ast, v)
        return v.defined, v.spreads

    # fallback regex (fine for machine-generated docs)
    defined = set(re.findall(r"fragment\s+(\w+)\s+on\b", doc))
    spreads = set(re.findall(r"\.\.\.\s*(\w+)", doc)) - {"on"}
    return defined, spreads


def bundle_operation_with_fragments(operation_text: str, *fragment_roots: Path) -> str:
    """Return operation_text + all recursively-referenced fragment definitions (deduped)."""
    outputs: list[str] = []
    errors: list[str] = []
    defined, spreads = _parse_defs_and_spreads(operation_text)

    visited: set[str] = set()
    stack = list(spreads - defined)

    while stack:
        name = stack.pop()
        if name in visited:
            continue
        frag_text = _load_fragment_text(name, fragment_roots).strip()
        if name.endswith("Error"):
            errors.append(strip_leading_comments(frag_text))
        else:
            outputs.append(strip_leading_comments(frag_text))
        visited.add(name)
        d2, s2 = _parse_defs_and_spreads(frag_text)
        stack.extend(sorted((s2 - d2) - visited))  # find nested fragments

    outputs.sort()
    out = outputs
    errors.sort()
    out.extend(errors)
    out.append(strip_leading_comments(operation_text.strip()))
    return "\n\n".join(out) + "\n"


def rpc_module(op_path: Path, full_document: str):
    return dedent(f'''\
# This file is automatically generated. DO NOT EDIT MANUALLY.

from __future__ import annotations

from typing import Any

import httpx
from fastapi.testclient import TestClient

from .runtime import AsyncTransport, Transport

DOCUMENT = """{full_document}"""


async def acall(
    transport: AsyncTransport,
    request: dict,
    token: str | None,
) -> dict:
    headers = httpx.Headers()
    if token:
        headers["Authorization"] = f"Bearer {{token}}"

    vars_dict: dict[str, Any] = {{'request': request}}
    payload = await transport.execute(DOCUMENT, vars_dict, headers=headers)
    return payload

def call(
    transport: Transport,
    request: dict,
    token: str | None,
) -> dict:
    headers = httpx.Headers()
    if token:
        headers["Authorization"] = f"Bearer {{token}}"

    vars_dict: dict[str, Any] = {{'request': request}}
    payload = transport.execute(DOCUMENT, vars_dict, headers=headers)
    return payload

def test(
    test_client: TestClient,
    request: dict,
    token: str | None,
) -> httpx.Response:
    headers = httpx.Headers()
    if token:
        headers["Authorization"] = f"Bearer {{token}}"

    vars_dict: dict[str, Any] = {{'request': request}}
    response = test_client.post(
        "/graphql",
        headers=headers,
        json={{"query": DOCUMENT, "variables": vars_dict}},
    )
    return response
''')


def strip_leading_comments(text: str) -> str:
    # also strip an optional UTF-8 BOM if present
    text = text.lstrip("\ufeff")

    lines = text.splitlines(keepends=True)  # preserve newlines
    i = 0
    while i < len(lines):
        stripped = lines[i].lstrip()
        if stripped.startswith("#") or stripped == "\n" or stripped == "\r\n":
            i += 1
            continue
        break
    return "".join(lines[i:])


def write_module_init(out_dir: Path):
    (out_dir / "__init__.py").write_text(
        dedent(
            """
            # This file is automatically generated. DO NOT EDIT MANUALLY.

            from .runtime import Transport, AsyncTransport
            """
        ).lstrip(),
        encoding="utf-8",
    )


def write_module(
    out_dir: Path,
    graphql_fragments_dir: Path,
    graphql_file_path: Path,
    schema: GraphQLSchema,
):
    module_name = inflection.underscore(graphql_file_path.stem)
    graphql_file_text = graphql_file_path.read_text(encoding="utf-8")
    full_document = bundle_operation_with_fragments(
        graphql_file_text,
        graphql_fragments_dir,  # any roots where fragments live
    )

    (out_dir / f"{module_name}.py").write_text(
        rpc_module(graphql_file_path, full_document),
        encoding="utf-8",
    )
