"""
Export pipeline for nblite.

Handles exporting notebooks to different formats and to Python modules.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path

from nblite.config.schema import ExportMode
from nblite.core.notebook import Format, Notebook
from nblite.export.function_export import export_function_notebook, is_function_notebook
from nblite.extensions import HookRegistry, HookType

__all__ = [
    "export_notebook_to_notebook",
    "export_notebook_to_module",
    "ExportResult",
]


# Pattern to extract function/class names for __all__
FUNCTION_PATTERN = re.compile(r"^(?:async\s+)?def\s+(\w+)\s*\(", re.MULTILINE)
CLASS_PATTERN = re.compile(r"^class\s+(\w+)\s*[\(:]", re.MULTILINE)
VARIABLE_PATTERN = re.compile(r"^([A-Z][A-Z0-9_]*)\s*=", re.MULTILINE)

# Pattern to match "from package.module import ..." statements
# Captures: (1) the package.module part, (2) the rest after "import"
FROM_IMPORT_PATTERN = re.compile(
    r"^(\s*)from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import\s+(.+)$",
    re.MULTILINE,
)


@dataclass
class ExportResult:
    """Result of an export operation."""

    success: bool = True
    files_created: list[Path] = field(default_factory=list)
    files_updated: list[Path] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)


def export_notebook_to_notebook(
    notebook: Notebook,
    output_path: Path | str,
    format: str | None = None,
) -> None:
    """
    Export a notebook to another notebook format.

    Args:
        notebook: Source notebook
        output_path: Output path for the notebook
        format: Output format (ipynb, percent). Auto-detected if None.
    """
    output_path = Path(output_path)

    if format is None:
        format = Format.from_path(output_path)

    content = notebook.to_string(format)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(content)


def export_notebook_to_module(
    notebook: Notebook,
    output_path: Path | str,
    project_root: Path | str,
    export_mode: ExportMode = ExportMode.PERCENT,
    include_warning: bool = True,
    cell_reference_style: str = "relative",
    package_name: str | None = None,
) -> None:
    """
    Export a notebook to a Python module.

    Args:
        notebook: Source notebook
        output_path: Output path for the module
        project_root: Project root directory for computing relative paths
        export_mode: Export mode (percent with cell markers, or py without)
        include_warning: Include autogenerated warning header
        cell_reference_style: Style for cell references (relative or absolute)
        package_name: Package name for converting absolute imports to relative imports.
            If provided, imports like "from {package_name}.X import Y" will be
            converted to relative imports like "from .X import Y".
    """
    output_path = Path(output_path)
    project_root = Path(project_root)

    # Check if this is a function notebook
    if is_function_notebook(notebook):
        export_function_notebook(
            notebook,
            output_path,
            include_warning=include_warning,
            package_name=package_name,
        )
        return

    source_path = notebook.source_path

    # Calculate relative path for cell references
    if source_path and cell_reference_style == "relative":
        try:
            source_ref = str(source_path.relative_to(project_root))
        except ValueError:
            source_ref = str(source_path)
    else:
        source_ref = str(source_path) if source_path else "unknown"

    # Calculate module depth for relative imports
    # e.g., my_lib/submodule/utils.py has depth 1 (one subdirectory)
    module_depth = 0
    if package_name:
        try:
            # Get the path relative to project root, then relative to package
            rel_to_project = output_path.relative_to(project_root)
            # The package is the first part, count remaining directory depth
            parts = rel_to_project.parts
            if parts and parts[0] == package_name:
                # Subtract 1 for package dir, subtract 1 for filename
                module_depth = len(parts) - 2
        except ValueError:
            pass

    # Collect exported cells
    exported_content = _collect_exported_content(
        notebook, export_mode, source_ref, package_name, module_depth
    )

    # Build module content
    lines: list[str] = []

    # Add autogenerated header
    if include_warning:
        lines.append(f"# AUTOGENERATED! DO NOT EDIT! File to edit: {source_ref}")
        lines.append("")

    # Add __all__ list
    all_names = _extract_public_names(exported_content)
    if all_names:
        all_str = ", ".join(f"'{name}'" for name in sorted(all_names))
        lines.append(f"__all__ = [{all_str}]")
        lines.append("")

    # Add content
    lines.append(exported_content)

    # Write output
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text("\n".join(lines))


def _collect_exported_content(
    notebook: Notebook,
    export_mode: ExportMode,
    source_ref: str,
    package_name: str | None = None,
    module_depth: int = 0,
) -> str:
    """
    Collect content from exported cells.

    Args:
        notebook: Source notebook
        export_mode: Export mode (percent or py)
        source_ref: Source reference string for cell markers
        package_name: Package name for import transformation
        module_depth: Depth of module within package (for relative imports)

    Hooks triggered:
        PRE_CELL_EXPORT: Before each cell export (cell=cell, notebook=notebook)
        POST_CELL_EXPORT: After each cell export (cell=cell, notebook=notebook, source=str)
    """
    parts: list[str] = []

    for cell in notebook.cells:
        if not cell.is_code:
            continue

        # Check for export directives
        has_export = cell.has_directive("export") or cell.has_directive("exporti")
        has_export_to = cell.has_directive("export_to")

        if not (has_export or has_export_to):
            continue

        # Get source without directives
        source = cell.source_without_directives.strip()
        if not source:
            continue

        # Transform absolute imports to relative imports
        if package_name:
            source = _transform_imports(source, package_name, module_depth)

        # Trigger PRE_CELL_EXPORT hook
        HookRegistry.trigger(
            HookType.PRE_CELL_EXPORT,
            cell=cell,
            notebook=notebook,
        )

        if export_mode == ExportMode.PERCENT:
            # Add cell marker
            parts.append(f"# %% {source_ref} {cell.index}")
            parts.append(source)
            parts.append("")
        else:
            # Plain Python mode
            parts.append(source)
            parts.append("")

        # Trigger POST_CELL_EXPORT hook
        HookRegistry.trigger(
            HookType.POST_CELL_EXPORT,
            cell=cell,
            notebook=notebook,
            source=source,
        )

    return "\n".join(parts)


def _transform_imports(source: str, package_name: str, module_depth: int) -> str:
    """
    Transform absolute imports to relative imports.

    Converts imports like "from {package_name}.X import Y" to relative imports
    like "from .X import Y" (with appropriate number of dots based on depth).

    Args:
        source: Source code to transform
        package_name: The package name to convert (e.g., "my_lib")
        module_depth: Depth of module within package (0 = directly in package)
            - my_lib/core.py has depth 0 -> needs 1 dot
            - my_lib/submodule/utils.py has depth 1 -> needs 2 dots

    Returns:
        Transformed source code
    """
    # Number of dots needed: depth + 1
    # depth 0 (my_lib/core.py) -> 1 dot (.)
    # depth 1 (my_lib/submodule/utils.py) -> 2 dots (..)
    num_dots = module_depth + 1
    dots = "." * num_dots

    def replace_import(match: re.Match) -> str:
        indent = match.group(1)
        module_path = match.group(2)
        imports = match.group(3)

        # Check if this import is from our package
        if module_path == package_name:
            # from my_lib import X -> from .. import X (with appropriate dots based on depth)
            return f"{indent}from {dots} import {imports}"
        elif module_path.startswith(f"{package_name}."):
            # from my_lib.core import X -> from .core import X (with appropriate dots)
            rest = module_path[len(package_name) + 1 :]  # Remove "my_lib."
            return f"{indent}from {dots}{rest} import {imports}"
        else:
            # Not our package, leave unchanged
            return match.group(0)

    return FROM_IMPORT_PATTERN.sub(replace_import, source)


def _extract_public_names(content: str) -> list[str]:
    """Extract public names (functions, classes, constants) from content."""
    names: list[str] = []

    # Find functions
    for match in FUNCTION_PATTERN.finditer(content):
        name = match.group(1)
        if not name.startswith("_"):
            names.append(name)

    # Find classes
    for match in CLASS_PATTERN.finditer(content):
        name = match.group(1)
        if not name.startswith("_"):
            names.append(name)

    # Find constants (UPPERCASE variables)
    for match in VARIABLE_PATTERN.finditer(content):
        name = match.group(1)
        names.append(name)

    return names
