"""Building the app and initializing all prerequisites."""

from __future__ import annotations

import json
import os
import subprocess
import zipfile
from pathlib import Path

from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn

from reflex import constants
from reflex.config import get_config
from reflex.utils import console, path_ops, prerequisites, processes


def set_env_json():
    """Write the upload url to a REFLEX_JSON."""
    path_ops.update_json_file(
        str(prerequisites.get_web_dir() / constants.Dirs.ENV_JSON),
        {endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint},
    )


def set_os_env(**kwargs):
    """Set os environment variables.

    Args:
        kwargs: env key word args.
    """
    for key, value in kwargs.items():
        if not value:
            continue
        os.environ[key.upper()] = value


def generate_sitemap_config(deploy_url: str, export=False):
    """Generate the sitemap config file.

    Args:
        deploy_url: The URL of the deployed app.
        export: If the sitemap are generated for an export.
    """
    # Import here to avoid circular imports.
    from reflex.compiler import templates

    config = {
        "siteUrl": deploy_url,
        "generateRobotsTxt": True,
    }

    if export:
        config["outDir"] = constants.Dirs.STATIC

    config = json.dumps(config)

    sitemap = prerequisites.get_web_dir() / constants.Next.SITEMAP_CONFIG_FILE
    sitemap.write_text(templates.SITEMAP_CONFIG(config=config))


def _zip(
    component_name: constants.ComponentName,
    target: str,
    root_dir: str,
    exclude_venv_dirs: bool,
    upload_db_file: bool = False,
    dirs_to_exclude: set[str] | None = None,
    files_to_exclude: set[str] | None = None,
    top_level_dirs_to_exclude: set[str] | None = None,
) -> None:
    """Zip utility function.

    Args:
        component_name: The name of the component: backend or frontend.
        target: The target zip file.
        root_dir: The root directory to zip.
        exclude_venv_dirs: Whether to exclude venv directories.
        upload_db_file: Whether to include local sqlite db files.
        dirs_to_exclude: The directories to exclude.
        files_to_exclude: The files to exclude.
        top_level_dirs_to_exclude: The top level directory names immediately under root_dir to exclude. Do not exclude folders by these names further in the sub-directories.

    """
    dirs_to_exclude = dirs_to_exclude or set()
    files_to_exclude = files_to_exclude or set()
    files_to_zip: list[str] = []
    # Traverse the root directory in a top-down manner. In this traversal order,
    # we can modify the dirs list in-place to remove directories we don't want to include.
    for root, dirs, files in os.walk(root_dir, topdown=True):
        # Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
        dirs[:] = [
            d
            for d in dirs
            if (basename := os.path.basename(os.path.normpath(d)))
            not in dirs_to_exclude
            and not basename.startswith(".")
            and (
                not exclude_venv_dirs or not _looks_like_venv_dir(os.path.join(root, d))
            )
        ]
        # If we are at the top level with root_dir, exclude the top level dirs.
        if top_level_dirs_to_exclude and root == root_dir:
            dirs[:] = [d for d in dirs if d not in top_level_dirs_to_exclude]
        # Modify the files in-place so the hidden files and db files are excluded.
        files[:] = [
            f
            for f in files
            if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
        ]
        files_to_zip += [
            os.path.join(root, file) for file in files if file not in files_to_exclude
        ]

    # Create a progress bar for zipping the component.
    progress = Progress(
        *Progress.get_default_columns()[:-1],
        MofNCompleteColumn(),
        TimeElapsedColumn(),
    )
    task = progress.add_task(
        f"Zipping {component_name.value}:", total=len(files_to_zip)
    )

    with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
        for file in files_to_zip:
            console.debug(f"{target}: {file}", progress=progress)
            progress.advance(task)
            zipf.write(file, os.path.relpath(file, root_dir))


def zip_app(
    frontend: bool = True,
    backend: bool = True,
    zip_dest_dir: str = os.getcwd(),
    upload_db_file: bool = False,
):
    """Zip up the app.

    Args:
        frontend: Whether to zip up the frontend app.
        backend: Whether to zip up the backend app.
        zip_dest_dir: The directory to export the zip file to.
        upload_db_file: Whether to upload the database file.
    """
    files_to_exclude = {
        constants.ComponentName.FRONTEND.zip(),
        constants.ComponentName.BACKEND.zip(),
    }

    if frontend:
        _zip(
            component_name=constants.ComponentName.FRONTEND,
            target=os.path.join(zip_dest_dir, constants.ComponentName.FRONTEND.zip()),
            root_dir=str(prerequisites.get_web_dir() / constants.Dirs.STATIC),
            files_to_exclude=files_to_exclude,
            exclude_venv_dirs=False,
        )

    if backend:
        _zip(
            component_name=constants.ComponentName.BACKEND,
            target=os.path.join(zip_dest_dir, constants.ComponentName.BACKEND.zip()),
            root_dir=".",
            dirs_to_exclude={"__pycache__"},
            files_to_exclude=files_to_exclude,
            top_level_dirs_to_exclude={"assets"},
            exclude_venv_dirs=True,
            upload_db_file=upload_db_file,
        )


def build(
    deploy_url: str | None = None,
    for_export: bool = False,
):
    """Build the app for deployment.

    Args:
        deploy_url: The deployment URL.
        for_export: Whether the build is for export.
    """
    wdir = prerequisites.get_web_dir()

    # Clean the static directory if it exists.
    path_ops.rm(str(wdir / constants.Dirs.STATIC))

    # The export command to run.
    command = "export"

    checkpoints = [
        "Linting and checking ",
        "Creating an optimized production build",
        "Route (pages)",
        "prerendered as static HTML",
        "Collecting page data",
        "Finalizing page optimization",
        "Collecting build traces",
    ]

    # Generate a sitemap if a deploy URL is provided.
    if deploy_url is not None:
        generate_sitemap_config(deploy_url, export=for_export)
        command = "export-sitemap"

        checkpoints.extend(["Loading next-sitemap", "Generation completed"])

    # Start the subprocess with the progress bar.
    process = processes.new_process(
        [prerequisites.get_package_manager(), "run", command],
        cwd=wdir,
        shell=constants.IS_WINDOWS,
    )
    processes.show_progress("Creating Production Build", process, checkpoints)


def setup_frontend(
    root: Path,
    disable_telemetry: bool = True,
):
    """Set up the frontend to run the app.

    Args:
        root: The root path of the project.
        disable_telemetry: Whether to disable the Next telemetry.
    """
    # Create the assets dir if it doesn't exist.
    path_ops.mkdir(constants.Dirs.APP_ASSETS)

    # Copy asset files to public folder.
    path_ops.cp(
        src=str(root / constants.Dirs.APP_ASSETS),
        dest=str(root / prerequisites.get_web_dir() / constants.Dirs.PUBLIC),
    )

    # Set the environment variables in client (env.json).
    set_env_json()

    # Disable the Next telemetry.
    if disable_telemetry:
        processes.new_process(
            [
                prerequisites.get_package_manager(),
                "run",
                "next",
                "telemetry",
                "disable",
            ],
            cwd=prerequisites.get_web_dir(),
            stdout=subprocess.DEVNULL,
            shell=constants.IS_WINDOWS,
        )


def setup_frontend_prod(
    root: Path,
    disable_telemetry: bool = True,
):
    """Set up the frontend for prod mode.

    Args:
        root: The root path of the project.
        disable_telemetry: Whether to disable the Next telemetry.
    """
    setup_frontend(root, disable_telemetry)
    build(deploy_url=get_config().deploy_url)


def _looks_like_venv_dir(dir_to_check: str) -> bool:
    return os.path.exists(os.path.join(dir_to_check, "pyvenv.cfg"))
