"""
Manage LangChain apps
"""

import shutil
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import typer
from typing_extensions import Annotated

from langchain_cli.utils.events import create_events
from langchain_cli.utils.git import (
    DependencySource,
    copy_repo,
    parse_dependencies,
    update_repo,
)
from langchain_cli.utils.packages import (
    LangServeExport,
    get_langserve_export,
    get_package_root,
)
from langchain_cli.utils.pyproject import (
    add_dependencies_to_pyproject_toml,
    remove_dependencies_from_pyproject_toml,
)

REPO_DIR = Path(typer.get_app_dir("langchain")) / "git_repos"

app_cli = typer.Typer(no_args_is_help=True, add_completion=False)


@app_cli.command()
def new(
    name: Annotated[
        Optional[str],
        typer.Argument(
            help="The name of the folder to create",
        ),
    ] = None,
    *,
    package: Annotated[
        Optional[List[str]],
        typer.Option(help="Packages to seed the project with"),
    ] = None,
    pip: Annotated[
        Optional[bool],
        typer.Option(
            "--pip/--no-pip",
            help="Pip install the template(s) as editable dependencies",
            is_flag=True,
        ),
    ] = None,
    noninteractive: Annotated[
        bool,
        typer.Option(
            "--non-interactive/--interactive",
            help="Don't prompt for any input",
            is_flag=True,
        ),
    ] = False,
):
    """
    Create a new LangServe application.
    """
    has_packages = package is not None and len(package) > 0

    if noninteractive:
        if name is None:
            raise typer.BadParameter("name is required when --non-interactive is set")
        name_str = name
        pip_bool = bool(pip)  # None should be false
    else:
        name_str = (
            name if name else typer.prompt("What folder would you like to create?")
        )
        if not has_packages:
            package = []
            package_prompt = "What package would you like to add? (leave blank to skip)"
            while True:
                package_str = typer.prompt(
                    package_prompt, default="", show_default=False
                )
                if not package_str:
                    break
                package.append(package_str)
                package_prompt = (
                    f"{len(package)} added. Any more packages (leave blank to end)?"
                )

            has_packages = len(package) > 0

        pip_bool = False
        if pip is None and has_packages:
            pip_bool = typer.confirm(
                "Would you like to install these templates into your environment "
                "with pip?",
                default=False,
            )
    # copy over template from ../project_template
    project_template_dir = Path(__file__).parents[1] / "project_template"
    destination_dir = Path.cwd() / name_str if name_str != "." else Path.cwd()
    app_name = name_str if name_str != "." else Path.cwd().name
    shutil.copytree(project_template_dir, destination_dir, dirs_exist_ok=name == ".")

    readme = destination_dir / "README.md"
    readme_contents = readme.read_text()
    readme.write_text(readme_contents.replace("__app_name__", app_name))

    pyproject = destination_dir / "pyproject.toml"
    pyproject_contents = pyproject.read_text()
    pyproject.write_text(pyproject_contents.replace("__app_name__", app_name))

    # add packages if specified
    if has_packages:
        add(package, project_dir=destination_dir, pip=pip_bool)


@app_cli.command()
def add(
    dependencies: Annotated[
        Optional[List[str]], typer.Argument(help="The dependency to add")
    ] = None,
    *,
    api_path: Annotated[List[str], typer.Option(help="API paths to add")] = [],
    project_dir: Annotated[
        Optional[Path], typer.Option(help="The project directory")
    ] = None,
    repo: Annotated[
        List[str],
        typer.Option(help="Install templates from a specific github repo instead"),
    ] = [],
    branch: Annotated[
        List[str], typer.Option(help="Install templates from a specific branch")
    ] = [],
    pip: Annotated[
        bool,
        typer.Option(
            "--pip/--no-pip",
            help="Pip install the template(s) as editable dependencies",
            is_flag=True,
            prompt="Would you like to `pip install -e` the template(s)?",
        ),
    ],
):
    """
    Adds the specified template to the current LangServe app.

    e.g.:
    langchain app add extraction-openai-functions
    langchain app add git+ssh://git@github.com/efriis/simple-pirate.git
    """

    parsed_deps = parse_dependencies(dependencies, repo, branch, api_path)

    project_root = get_package_root(project_dir)

    package_dir = project_root / "packages"

    create_events(
        [{"event": "serve add", "properties": dict(parsed_dep=d)} for d in parsed_deps]
    )

    # group by repo/ref
    grouped: Dict[Tuple[str, Optional[str]], List[DependencySource]] = {}
    for dep in parsed_deps:
        key_tup = (dep["git"], dep["ref"])
        lst = grouped.get(key_tup, [])
        lst.append(dep)
        grouped[key_tup] = lst

    installed_destination_paths: List[Path] = []
    installed_destination_names: List[str] = []
    installed_exports: List[LangServeExport] = []

    for (git, ref), group_deps in grouped.items():
        if len(group_deps) == 1:
            typer.echo(f"Adding {git}@{ref}...")
        else:
            typer.echo(f"Adding {len(group_deps)} templates from {git}@{ref}")
        source_repo_path = update_repo(git, ref, REPO_DIR)

        for dep in group_deps:
            source_path = (
                source_repo_path / dep["subdirectory"]
                if dep["subdirectory"]
                else source_repo_path
            )
            pyproject_path = source_path / "pyproject.toml"
            if not pyproject_path.exists():
                typer.echo(f"Could not find {pyproject_path}")
                continue
            langserve_export = get_langserve_export(pyproject_path)

            # default path to package_name
            inner_api_path = dep["api_path"] or langserve_export["package_name"]

            destination_path = package_dir / inner_api_path
            if destination_path.exists():
                typer.echo(
                    f"Folder {str(inner_api_path)} already exists. " "Skipping...",
                )
                continue
            copy_repo(source_path, destination_path)
            typer.echo(f" - Downloaded {dep['subdirectory']} to {inner_api_path}")
            installed_destination_paths.append(destination_path)
            installed_destination_names.append(inner_api_path)
            installed_exports.append(langserve_export)

    if len(installed_destination_paths) == 0:
        typer.echo("No packages installed. Exiting.")
        return

    try:
        add_dependencies_to_pyproject_toml(
            project_root / "pyproject.toml",
            zip(installed_destination_names, installed_destination_paths),
        )
    except Exception:
        # Can fail if user modified/removed pyproject.toml
        typer.echo("Failed to add dependencies to pyproject.toml, continuing...")

    try:
        cwd = Path.cwd()
        installed_destination_strs = [
            str(p.relative_to(cwd)) for p in installed_destination_paths
        ]
    except ValueError:
        # Can fail if the cwd is not a parent of the package
        typer.echo("Failed to print install command, continuing...")
    else:
        if pip:
            cmd = ["pip", "install", "-e"] + installed_destination_strs
            cmd_str = " \\\n  ".join(installed_destination_strs)
            typer.echo(f"Running: pip install -e \\\n  {cmd_str}")
            subprocess.run(cmd, cwd=cwd)

    chain_names = []
    for e in installed_exports:
        original_candidate = f'{e["package_name"].replace("-", "_")}_chain'
        candidate = original_candidate
        i = 2
        while candidate in chain_names:
            candidate = original_candidate + "_" + str(i)
            i += 1
        chain_names.append(candidate)

    api_paths = [
        str(Path("/") / path.relative_to(package_dir))
        for path in installed_destination_paths
    ]

    imports = [
        f"from {e['module']} import {e['attr']} as {name}"
        for e, name in zip(installed_exports, chain_names)
    ]
    routes = [
        f'add_routes(app, {name}, path="{path}")'
        for name, path in zip(chain_names, api_paths)
    ]

    t = (
        "this template"
        if len(chain_names) == 1
        else f"these {len(chain_names)} templates"
    )
    lines = (
        ["", f"To use {t}, add the following to your app:\n\n```", ""]
        + imports
        + [""]
        + routes
        + ["```"]
    )
    typer.echo("\n".join(lines))


@app_cli.command()
def remove(
    api_paths: Annotated[List[str], typer.Argument(help="The API paths to remove")],
    *,
    project_dir: Annotated[
        Optional[Path], typer.Option(help="The project directory")
    ] = None,
):
    """
    Removes the specified package from the current LangServe app.
    """

    project_root = get_package_root(project_dir)

    project_pyproject = project_root / "pyproject.toml"

    package_root = project_root / "packages"

    remove_deps: List[str] = []

    for api_path in api_paths:
        package_dir = package_root / api_path
        if not package_dir.exists():
            typer.echo(f"Package {api_path} does not exist. Skipping...")
            continue
        try:
            pyproject = package_dir / "pyproject.toml"
            langserve_export = get_langserve_export(pyproject)
            typer.echo(f"Removing {langserve_export['package_name']}...")

            shutil.rmtree(package_dir)
            remove_deps.append(api_path)
        except Exception:
            pass

    try:
        remove_dependencies_from_pyproject_toml(project_pyproject, remove_deps)
    except Exception:
        # Can fail if user modified/removed pyproject.toml
        typer.echo("Failed to remove dependencies from pyproject.toml.")


@app_cli.command()
def serve(
    *,
    port: Annotated[
        Optional[int], typer.Option(help="The port to run the server on")
    ] = None,
    host: Annotated[
        Optional[str], typer.Option(help="The host to run the server on")
    ] = None,
    app: Annotated[
        Optional[str], typer.Option(help="The app to run, e.g. `app.server:app`")
    ] = None,
) -> None:
    """
    Starts the LangServe app.
    """

    # add current dir as first entry of path
    sys.path.append(str(Path.cwd()))

    app_str = app if app is not None else "app.server:app"
    host_str = host if host is not None else "127.0.0.1"

    import uvicorn

    uvicorn.run(
        app_str, host=host_str, port=port if port is not None else 8000, reload=True
    )
