""" Zensical documentation generation utilities.

This module provides a comprehensive set of utilities for automatically generating
and managing documentation for Python projects using **Zensical** (a modern static
site generator based on MkDocs Material) and **mkdocstrings** for API reference
generation from docstrings.

It handles the creation of configuration files, index pages, API reference pages,
version management, and HTML generation.

Example of usage:

.. code-block:: python

    import stouputils as stp
    from stouputils.applications import automatic_docs

    if __name__ == "__main__":
        automatic_docs.zensical_docs(
            root_path=stp.get_root_path(__file__, go_up=1),
            project="stouputils",
            author="Stoupy",
            copyright="2025, Stoupy",
            html_logo="https://avatars.githubusercontent.com/u/35665974",
            html_favicon="https://avatars.githubusercontent.com/u/35665974",
            github_user="Stoupy51",
            github_repo="stouputils",
            version="1.2.0",
            skip_undocumented=True,
        )
"""
# Imports
import os
import shutil
import subprocess
import sys
from collections.abc import Callable

from ...decorators import LogLevels, handle_error
from ...io.path import clean_path, super_open
from ...print.message import info, warning
from .common import check_base_dependencies, download_asset, generate_redirect_html, generate_version_selector, get_versions_from_github


# Functions
def get_zensical_config_content(
	project: str,
	project_dir: str,
	docs_dir: str,
	site_dir: str,
	author: str,
	current_version: str,
	copyright: str,
	html_logo: str,
	html_favicon: str,
	github_user: str = "",
	github_repo: str = "",
	version_list: list[str] | None = None,
	skip_undocumented: bool = True,
	api_pages: list[tuple[str, str]] | None = None,
) -> str:
	""" Get the content of the Zensical configuration file (zensical.toml).

	Args:
		project           (str):              Name of the project
		project_dir       (str):              Path to the project directory
		docs_dir          (str):              Path to the docs source directory (relative to root)
		site_dir          (str):              Path to the build output directory (relative to root)
		author            (str):              Author of the project
		current_version   (str):              Current version
		copyright         (str):              Copyright information
		html_logo         (str):              Path to the logo image (relative to docs_dir)
		html_favicon      (str):              Path to the favicon image (relative to docs_dir)
		github_user       (str):              GitHub username
		github_repo       (str):              GitHub repository name
		version_list      (list[str] | None): List of versions. Defaults to None
		skip_undocumented (bool):             Whether to skip undocumented members. Defaults to True
		api_pages         (list[tuple[str, str]] | None): List of (module_name, md_filename) tuples for nav

	Returns:
		str: Content of the Zensical configuration file (TOML)
	"""
	parent_of_project_dir: str = clean_path(os.path.dirname(project_dir))

	# Build repo URL
	repo_url: str = f"https://github.com/{github_user}/{github_repo}" if github_user and github_repo else ""
	repo_name: str = f"{github_user}/{github_repo}" if github_user and github_repo else ""

	# Project section
	config: str = f"""[project]
site_name = "{project}"
site_description = "{project} documentation — v{current_version}"
site_author = "{author}"
copyright = "Copyright &copy; {copyright}"
docs_dir = "{docs_dir}"
site_dir = "{site_dir}"
"""
	if repo_url:
		config += f'repo_url = "{repo_url}"\n'
		config += f'repo_name = "{repo_name}"\n'

	config += 'extra_css = ["_static/custom.css"]\n'

	# Navigation
	if api_pages:
		nav_items: list[str] = []
		for module_name, md_filename in api_pages:
			nav_items.append(f'        {{ "{module_name}" = "api/{md_filename}" }}')
		api_nav: str = ",\n".join(nav_items)
		config += f"""
nav = [
    {{ "Home" = "index.md" }},
    {{ "API Reference" = [
{api_nav},
    ] }},
]
"""

	# Theme section
	config += """
[project.theme]
language = "en"
"""
	if html_favicon:
		config += f'favicon = "{html_favicon}"\n'
	if html_logo:
		config += f'logo = "{html_logo}"\n'

	config += """
features = [
    "content.code.copy",
    "content.code.annotate",
    "content.code.select",
    "content.tooltips",
    "navigation.footer",
    "navigation.instant",
    "navigation.instant.prefetch",
    "navigation.sections",
    "navigation.top",
    "navigation.path",
    "navigation.indexes",
    "search.highlight",
]

[[project.theme.palette]]
scheme = "slate"
toggle.icon = "lucide/sun"
toggle.name = "Switch to light mode"

[[project.theme.palette]]
scheme = "default"
toggle.icon = "lucide/moon"
toggle.name = "Switch to dark mode"
"""

	# mkdocstrings plugin configuration
	show_if_no_docstring: str = "false" if skip_undocumented else "true"
	config += f"""
[project.plugins.mkdocstrings.handlers.python]
paths = ["{parent_of_project_dir}"]

[project.plugins.mkdocstrings.handlers.python.options]
docstring_style = "google"
show_source = true
members_order = "source"
show_root_heading = true
show_symbol_type_heading = true
show_symbol_type_toc = true
show_if_no_docstring = {show_if_no_docstring}
inherited_members = true
merge_init_into_class = true
group_by_category = true
"""
	return config

def generate_index_md(
	readme_path: str,
	index_path: str,
	project: str,
	github_user: str,
	github_repo: str,
	get_versions_function: Callable[[str, str, int], list[str]] = get_versions_from_github,
	recent_minor_versions: int = 2,
) -> None:
	""" Generate ``index.md`` from README.md content.

	This keeps the README content as Markdown and creates the home page for the
	Zensical documentation. Navigation to API docs is handled by the Zensical
	config (nav or auto-discovery).

	Args:
		readme_path            (str): Path to the README.md file
		index_path             (str): Path where index.md should be created
		project                (str): Name of the project
		github_user            (str): GitHub username
		github_repo            (str): GitHub repository name
		get_versions_function  (Callable[[str, str, int], list[str]]): Function to get versions from GitHub
		recent_minor_versions  (int): Number of recent minor versions to show all patches for. Defaults to 2
	"""
	# Read README content
	with open(readme_path, encoding="utf-8") as f:
		readme_content: str = f.read()

	# Generate version selector (markdown links)
	version_selector: str = generate_version_selector(
		github_user=github_user,
		github_repo=github_repo,
		get_versions_function=get_versions_function,
		recent_minor_versions=recent_minor_versions,
	)

	# Build final markdown content
	md_content: str = f"""# Welcome to {project.capitalize()} Documentation

{version_selector}

{readme_content}
"""

	# Write the Markdown file
	with open(index_path, "w", encoding="utf-8") as f:
		f.write(md_content)

def generate_api_pages(
	api_dir: str,
	project: str,
	project_dir: str,
) -> list[tuple[str, str]]:
	""" Walk the project directory and generate Markdown pages with ``:::`` directives
	for mkdocstrings API documentation.

	Args:
		api_dir      (str): Directory where API markdown pages should be written
		project      (str): Name of the project (package name)
		project_dir  (str): Path to the project directory (Python package root)

	Returns:
		list[tuple[str, str]]: List of (module_name, md_filename) tuples
	"""
	pages: list[tuple[str, str]] = []
	project_dir = clean_path(project_dir)
	parent_dir: str = os.path.dirname(project_dir)
	os.makedirs(api_dir, exist_ok=True)

	for root, dirs, files in os.walk(project_dir):
		# Skip __pycache__ and sort directories for consistent ordering
		dirs[:] = sorted(d for d in dirs if d != "__pycache__")

		# Compute the Python module name from path
		rel_path: str = os.path.relpath(root, parent_dir).replace(os.sep, "/")
		module_name: str = rel_path.replace("/", ".")

		# Only process Python packages (directories with __init__.py)
		if "__init__.py" not in files:
			continue

		# Create page for the package itself
		md_filename: str = f"{module_name}.md"
		md_path: str = f"{api_dir}/{md_filename}"
		with open(md_path, "w", encoding="utf-8") as f:
			f.write(f"# {module_name}\n\n::: {module_name}\n")
		pages.append((module_name, md_filename))

		# Create pages for individual submodules (non-__init__ .py files)
		for fname in sorted(files):
			if fname.endswith(".py") and fname != "__init__.py":
				sub_module: str = fname[:-3]
				full_module: str = f"{module_name}.{sub_module}"
				sub_md_filename: str = f"{full_module}.md"
				sub_md_path: str = f"{api_dir}/{sub_md_filename}"
				with open(sub_md_path, "w", encoding="utf-8") as f:
					f.write(f"# {full_module}\n\n::: {full_module}\n")
				pages.append((full_module, sub_md_filename))

	return pages

def generate_documentation(
	config_path: str,
	root_path: str,
) -> None:
	""" Generate documentation using Zensical.

	Args:
		config_path (str): Path to the zensical.toml configuration file
		root_path   (str): Root path of the project (working directory for the build)
	"""
	# Find the zensical executable
	zensical_cmd: str | None = shutil.which("zensical")
	if not zensical_cmd:
		raise RuntimeError(
			"'zensical' command not found in PATH. "
			"Install it with: pip install zensical"
		)

	# Run zensical build
	info(f"Running: {zensical_cmd} build -f {config_path} --clean")
	result: subprocess.CompletedProcess[bytes] = subprocess.run(
		[zensical_cmd, "build", "-f", config_path, "--clean"],
		cwd=root_path,
		stdout=sys.stdout,
		stderr=sys.stderr,
	)
	if result.returncode != 0:
		raise RuntimeError(f"Zensical build failed (exit code {result.returncode})")

@handle_error(error_log=LogLevels.WARNING_TRACEBACK)
def zensical_docs(
	root_path: str,
	project: str,
	project_dir: str = "",
	author: str = "Author",
	copyright: str = "2025, Author",
	html_logo: str = "",
	html_favicon: str = "",
	html_theme: str = "",
	github_user: str = "",
	github_repo: str = "",
	version: str | None = None,
	skip_undocumented: bool = True,
	recent_minor_versions: int = 2,

	get_versions_function: Callable[[str, str, int], list[str]] = get_versions_from_github,
	generate_index_function: Callable[..., None] = generate_index_md,
	generate_api_pages_function: Callable[..., list[tuple[str, str]]] = generate_api_pages,
	generate_docs_function: Callable[..., None] = generate_documentation,
	generate_redirect_function: Callable[[str], None] = generate_redirect_html,
	get_config_content_function: Callable[..., str] = get_zensical_config_content,
) -> None:
	""" Update the documentation using Zensical and mkdocstrings.

	Args:
		root_path                    (str): Root path of the project
		project                      (str): Name of the project
		project_dir                  (str): Path to the project directory (to be used with generate_docs_function)
		author                       (str): Author of the project
		copyright                    (str): Copyright information
		html_logo                    (str): URL or path to the logo image
		html_favicon                 (str): URL or path to the favicon image
		html_theme                   (str): Unused (kept for backward compatibility)
		github_user                  (str): GitHub username
		github_repo                  (str): GitHub repository name
		version                      (str | None): Version to build documentation for (e.g. "1.0.0", defaults to "latest")
		skip_undocumented            (bool): Whether to skip undocumented members. Defaults to True
		recent_minor_versions        (int): Number of recent minor versions to show all patches for. Defaults to 2

		get_versions_function        (Callable[[str, str, int], list[str]]): Function to get versions from GitHub
		generate_index_function      (Callable[..., None]): Function to generate index.md
		generate_api_pages_function  (Callable[..., list[tuple[str, str]]]): Function to generate API pages
		generate_docs_function       (Callable[..., None]): Function to generate documentation
		generate_redirect_function   (Callable[[str], None]): Function to create redirect file
		get_config_content_function  (Callable[..., str]): Function to get Zensical config content
	"""
	if html_theme:
		warning("The 'html_theme' parameter is not used in Zensical docs generation (it uses its own built-in theme).")
	check_base_dependencies()

	# Setup paths
	root_path = clean_path(root_path)
	docs_dir: str = f"{root_path}/docs"
	source_dir: str = f"{docs_dir}/source"
	api_dir: str = f"{source_dir}/api"
	static_dir: str = f"{source_dir}/_static"
	html_dir: str = f"{docs_dir}/build/html"

	# Resolve project directory
	effective_project_dir: str = project_dir if project_dir else f"{root_path}/{project}"

	# Remove "v" from version if it is a string (just in case)
	version = version.replace("v", "") if isinstance(version, str) else version

	# Modify build directory if version is specified
	latest_dir: str = f"{html_dir}/latest"
	build_dir: str = latest_dir if not version else f"{html_dir}/v{version}"

	# Create directories
	for d in [api_dir, static_dir]:
		os.makedirs(d, exist_ok=True)

	# Download logo and favicon if they are URLs
	logo_ref: str = html_logo
	favicon_ref: str = html_favicon
	if html_logo and html_logo.startswith("http"):
		local_logo: str = f"{static_dir}/logo.png"
		download_asset(html_logo, local_logo)
		logo_ref = "_static/logo.png"
	if html_favicon and html_favicon.startswith("http"):
		local_favicon: str = f"{static_dir}/favicon.png"
		download_asset(html_favicon, local_favicon)
		favicon_ref = "_static/favicon.png"

	# Create custom CSS
	custom_css_path: str = f"{static_dir}/custom.css"
	with super_open(custom_css_path, "w") as f:
		f.write("""
/* Custom CSS for documentation */
/* Gradient animation keyframes */
@keyframes shine-slide {
	0% { background-position: -200% center; }
	100% { background-position: 200% center; }
}

/* On hover animation for links */
a:hover, a:hover span {
	background: linear-gradient(
		110deg,
		currentColor 0%,
		currentColor 40%,
		white 50%,
		currentColor 60%,
		currentColor 100%
	);
	background-size: 200% 100%;
	background-clip: text;
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
	animation: shine-slide 3.5s linear infinite;
}
""")

	# Generate index.md from README.md
	readme_path: str = f"{root_path}/README.md"
	index_path: str = f"{source_dir}/index.md"
	generate_index_function(
		readme_path=readme_path,
		index_path=index_path,
		project=project,
		github_user=github_user,
		github_repo=github_repo,
		get_versions_function=get_versions_function,
		recent_minor_versions=recent_minor_versions,
	)

	# Clean up old API documentation and regenerate
	if os.path.exists(api_dir):
		shutil.rmtree(api_dir)
	os.makedirs(api_dir, exist_ok=True)

	# Generate API pages with ::: directives for mkdocstrings
	api_pages: list[tuple[str, str]] = generate_api_pages_function(
		api_dir=api_dir,
		project=project,
		project_dir=effective_project_dir,
	)
	info(f"Generated {len(api_pages)} API documentation pages")

	# Get versions and current version for config
	version_list: list[str] = get_versions_function(github_user, github_repo, recent_minor_versions)
	current_version: str = version if version else "latest"

	# Generate zensical.toml config
	# Paths in zensical.toml are relative to the config file location (root_path)
	relative_docs_dir: str = os.path.relpath(source_dir, root_path).replace(os.sep, "/")
	relative_site_dir: str = os.path.relpath(build_dir, root_path).replace(os.sep, "/")

	config_content: str = get_config_content_function(
		project=project,
		project_dir=effective_project_dir,
		docs_dir=relative_docs_dir,
		site_dir=relative_site_dir,
		author=author,
		current_version=current_version,
		copyright=copyright,
		html_logo=logo_ref,
		html_favicon=favicon_ref,
		github_user=github_user,
		github_repo=github_repo,
		version_list=version_list,
		skip_undocumented=skip_undocumented,
		api_pages=api_pages,
	)
	config_path: str = f"{root_path}/zensical.toml"
	with open(config_path, "w", encoding="utf-8") as f:
		f.write(config_content)

	# Build documentation with Zensical
	generate_docs_function(
		config_path=config_path,
		root_path=root_path,
	)

	# Add index.html to the build directory that redirects to the latest version
	generate_redirect_function(f"{html_dir}/index.html")

	# If version is specified, copy the build directory to latest too
	if version:
		if os.path.exists(latest_dir):
			shutil.rmtree(latest_dir)
		shutil.copytree(build_dir, latest_dir, dirs_exist_ok=True)

	info("Documentation updated successfully!")
	info(f"You can view the documentation by opening {build_dir}/index.html")

