"""Tools for parsing ipynb files."""

from __future__ import annotations

from pathlib import Path

__all__ = ("convert_code_cells_to_app_py", "get_shiny_deps")

import ast
from typing import Literal, cast

from ._typing_extensions import NotRequired, TypedDict

QuartoShinyCodeCellClass = Literal["python", "r", "cell-code", "hidden"]
QuartoShinyCodeCellContext = Literal["ui", "server", "server-setup"]


class QuartoShinyCodeCell(TypedDict):
    text: str
    context: list[QuartoShinyCodeCellContext]
    classes: list[QuartoShinyCodeCellClass]


class QuartoShinyCodeCells(TypedDict):
    schema_version: int
    cells: list[QuartoShinyCodeCell]
    html_file: str


def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) -> None:
    """Parse an code cell JSON file and output an app.py file."""
    import json
    from textwrap import indent

    json_file = Path(json_file)
    app_file = Path(app_file)

    if app_file.exists():
        with open(app_file, "r", encoding="utf-8") as f:
            first_line = f.readline().strip()
            if first_line != "# This file generated by Quarto; do not edit by hand.":
                raise ValueError(
                    f"Not overwriting app file {app_file}, because it does not appear to be generated by Quarto. "
                    " If this is incorrect, remove the file and try again."
                )

    with open(json_file, "r", encoding="utf-8") as f:
        data = cast(QuartoShinyCodeCells, json.load(f))

    if data["schema_version"] != 1:
        raise ValueError("Only schema_version 1 is supported.")

    cells = data["cells"]

    session_code_cell_texts: list[str] = []
    global_code_cell_texts: list[str] = []

    for cell in cells:
        if "python" not in cell["classes"]:
            continue

        if "server-setup" in cell["context"]:
            global_code_cell_texts.append(cell["text"] + "\n\n# " + "=" * 72 + "\n\n")
        elif "server" in cell["context"]:
            validate_code_has_no_star_import(cell["text"])
            session_code_cell_texts.append(
                indent(cell["text"], "    ") + "\n\n    # " + "=" * 72 + "\n\n"
            )

    app_content = f"""# This file generated by Quarto; do not edit by hand.
# shiny_mode: core

from __future__ import annotations

from pathlib import Path
from shiny import App, Inputs, Outputs, Session, ui

{"".join(global_code_cell_texts)}


def server(input: Inputs, output: Outputs, session: Session) -> None:
{"".join(session_code_cell_texts)}

    return None


_static_assets = ##STATIC_ASSETS_PLACEHOLDER##
_static_assets = {{"/" + sa: Path(__file__).parent / sa for sa in _static_assets}}

app = App(
    Path(__file__).parent / "{data["html_file"]}",
    server,
    static_assets=_static_assets,
)
"""

    with open(app_file, "w", encoding="utf-8") as f:
        f.write(app_content)


# =============================================================================
# HTML Dependency types
# =============================================================================
class QuartoHtmlDepItem(TypedDict):
    name: str
    path: str
    attribs: NotRequired[dict[str, str]]


class QuartoHtmlDepServiceworkerItem(TypedDict):
    source: str
    destination: str


class QuartoHtmlDependency(TypedDict):
    name: str
    version: NotRequired[str]
    scripts: NotRequired[list[str | QuartoHtmlDepItem]]
    stylesheets: NotRequired[list[str | QuartoHtmlDepItem]]
    resources: NotRequired[list[QuartoHtmlDepItem]]
    meta: NotRequired[dict[str, str]]
    serviceworkers: NotRequired[list[QuartoHtmlDepServiceworkerItem]]


def placeholder_dep() -> QuartoHtmlDependency:
    return {
        "name": "shiny-dependency-placeholder",
        "version": "9.9.9",
        "meta": {"shiny-dependency-placeholder": ""},
    }


def get_shiny_deps() -> str:
    import json

    return json.dumps([placeholder_dep()], indent=2)


# =============================================================================
# Functions for checking if code has a star import
# =============================================================================
def validate_code_has_no_star_import(content: str) -> None:
    """
    Check if Python code has a star import at the top level and if so raise an error.

    Parameters
    ----------
    content
        A string with Python code.

    Returns
    -------
    :
        None
    """
    if code_has_star_import(content):
        raise ValueError(
            "'import *' statements cannot be used in a regular Shiny code block in Quarto.\n"
            "Please move '*' imports to a code block with '#| context: setup', or used named imports instead.\n"
        )


def code_has_star_import(content: str) -> bool:
    try:
        tree = ast.parse(content)
        detector = DetectImportStarVisitor()
        detector.visit(tree)

    except Exception:
        return False

    return detector.found_star_import


class DetectImportStarVisitor(ast.NodeVisitor):
    def __init__(self):
        super().__init__()
        self.found_star_import = False

    def visit_ImportFrom(self, node: ast.ImportFrom):
        if any(alias.name == "*" for alias in node.names):
            self.found_star_import = True

    # Visit top-level nodes.
    def visit_Module(self, node: ast.Module):
        super().generic_visit(node)

    # Don't recurse into any nodes, so the we'll only ever look at top-level nodes.
    def generic_visit(self, node: ast.AST):
        pass
