import typer
from collections.abc import Mapping


import click
import scioer.config.load as load
import scioer.config.parse as parser
import os
import re
from typing import Optional
from pathlib import Path
import logging

try:
    import readline
except:
    import sys

    if sys.platform == "win32" or sys.platform == "cygwin":
        try:
            from pyreadline3 import Readline
        except:
            pass

__version__ = "UNKNOWN"
try:
    from scioer.__version__ import __version__
except:
    pass

import scioer.docker as docker

_LOGGER = logging.getLogger(__name__)

app = typer.Typer(
    name="Self Contained Interactive Open Educational Resource Helper",
    help=""" A CLI tool to help configure, start, stop course resources.

    \b
    Common usage commands:
    1. `scioer config`
        .... fill in the form
    2. `scioer start <course>`
    3. `scioer shell <course>`
    4. `scioer stop <course>`
    \f
    """,
    no_args_is_help=True,
    context_settings={"help_option_names": ["-h", "--help"]},
)


def conf_callback(ctx: typer.Context, param: typer.CallbackParam, value: Path):
    if value:
        value = os.path.realpath(os.path.expanduser(str(value)))

    configFiles = load.get_config_files(value)
    config = load.filter_config_files(configFiles)

    if not value and not config:
        config = configFiles[0]
        _LOGGER.info(f"No config file found, using default: {config}")
    elif value and value != config:
        config = value
        _LOGGER.info(f"Config file does not exist yet, using anyway: {config}")

    if config:
        _LOGGER.info(f"Loading from config file: {config}")

        data = parser.load_config_file(config)

        ctx.default_map = ctx.default_map or {}  # Initialize the default map
        ctx.default_map.update(data)  # Merge the config dict into default_map

        ctx.obj = ctx.obj or {}  # Initialize the object
        ctx.obj.update(
            {"config_file": config, "config": data}
        )  # Merge the config dict into object

    return config


def version_callback(value: bool):
    if value:
        typer.echo(f"scioer CLI Version: {__version__}")
        raise typer.Exit()


configOption = typer.Option(
    None,
    "--config",
    "-c",
    metavar="FILE",
    dir_okay=False,
    resolve_path=False,
    readable=True,
    writable=True,
    callback=conf_callback,
    is_eager=True,
    help="Path to the yaml config file",
)

courseNameArgument = typer.Argument(
    None, metavar="COURSE_NAME", help="The name of the course"
)


def load_course(config: Mapping, courseName: str, ask: bool = True):
    course = config.get(courseName, {})

    while ask and not course:
        if courseName:
            typer.secho(
                f'Course "{courseName} is not found. use `scioer config` if you want to create it.',
                fg=typer.colors.YELLOW,
            )
        courses = [k for k in config.keys() if isinstance(config[k], dict)]
        courseName = click.prompt(
            "Course not found, did you mean one of:", type=click.Choice(courses)
        )

        course = config.get(courseName, {})

    if course:
        course["name"] = courseName

    _LOGGER.info(f"course content: {course}")
    return course


def print_post_start_help(courseName):
    typer.secho(f"Started the {courseName} course resource", fg=typer.colors.GREEN)
    typer.secho(
        f"Login for the wiki: 'admin@example.com' 'password'", fg=typer.colors.GREEN
    )

    typer.echo("-----")
    typer.secho(f"To stop the course: scioer stop {courseName}", fg=typer.colors.YELLOW)
    typer.secho(
        f"To get a shell for the course: scioer shell {courseName}",
        fg=typer.colors.YELLOW,
    )
    typer.secho(
        f"To re-start the course: scioer start {courseName}", fg=typer.colors.YELLOW
    )
    typer.secho(
        f"To get information on the courses: scioer status", fg=typer.colors.YELLOW
    )


@app.command()
def start(
    ctx: typer.Context,
    name: Optional[str] = courseNameArgument,
    pull: bool = False,
    configFile: Optional[Path] = configOption,
):
    """Start a oer container"""

    client = docker.setup()

    config = ctx.default_map
    if not name and len(config.keys()) == 1:
        name = list(config.keys())[0]

    course = load_course(config, name)

    if course.get("auto_pull", False) or pull:
        typer.secho(
            f"Pulling the latest version of the {course['name']}",
            fg=typer.colors.GREEN,
        )
        typer.secho(
            f"This may take a while...",
            fg=typer.colors.YELLOW,
        )
        docker.fetch_latest(client, course["image"])

    typer.secho("Starting...", fg=typer.colors.GREEN)
    docker.start_container(client, course)
    print_post_start_help(course["name"])


@app.command()
def stop(
    ctx: typer.Context,
    name: Optional[str] = courseNameArgument,
    keep: Optional[bool] = typer.Option(False, "--no-remove", "-k"),
    configFile: Optional[Path] = configOption,
):
    """Stop a running course container"""

    client = docker.setup()

    config = ctx.default_map
    if not name and len(config.keys()) == 1:
        name = list(config.keys())[0]

    course = load_course(config, name, ask=False)

    if course:
        typer.secho(
            f"Stopping course container, this make take a couple seconds...",
            fg=typer.colors.RED,
        )
        docker.stop_container(client, course["name"], keep)
    else:
        typer.secho(f'Course "{name}" is not running.', fg=typer.colors.YELLOW)


@app.command()
def shell(
    ctx: typer.Context,
    name: str = courseNameArgument,
    configFile: Optional[Path] = configOption,
):
    """Start a shell in a running course container"""

    client = docker.setup()

    config = ctx.default_map
    if not name and len(config.keys()) == 1:
        name = list(config.keys())[0]

    course = load_course(config, name, ask=False)

    if course:
        docker.attach(client, course["name"])
    else:
        typer.secho(f'Course "{name}" is not running.', fg=typer.colors.YELLOW)


def print_container(container):
    indent = 2
    print(f"Course: {container.name[7:]} ({container.status})")

    print(f'{" " * indent }Ports:')
    ports = list(container.ports.items())
    if ports:
        for port in ports[:-1]:
            print(f'{" " * indent }├── {port[1][0]["HostPort"]} -> {port[0][:-4]}')
        port = ports[-1]
        print(f'{" " * indent }└── {port[1][0]["HostPort"]} -> {port[0][:-4]}')

    print(f'{" " * indent }Volumes:')
    volumes = [v for v in container.attrs["Mounts"] if v["Type"] == "bind"]

    home = os.path.expanduser("~")
    if volumes:
        for volume in volumes[:-1]:
            hostPath = volume["Source"].replace(home, "~")
            print(f'{" " * indent }├── {hostPath} as {volume["Destination"]}')

        volume = volumes[-1]
        hostPath = volume["Source"].replace(home, "~")
        print(f'{" " * indent }└── {hostPath} as {volume["Destination"]}')


@app.command()
def status(
    ctx: typer.Context,
    configFile: Optional[Path] = configOption,
):
    """Prints all the information about the running container"""
    client = docker.setup()

    config = ctx.default_map
    config_file = ctx.obj["config_file"]
    _LOGGER.info(f"Config contents: {config}")
    names = list(config.keys())

    home = os.path.expanduser("~")
    configPath = config_file.replace(home, "~")
    print(f"Config file: {configPath}")

    # containers = client.containers.list(all=True)
    # containers = [c for c in containers if c.name.replace('scioer_', '') in names ]

    filtered = []
    notRunning = []
    for n in names:
        try:
            c = client.containers.get(f"scioer_{n}")
            _LOGGER.debug(f"Container information for course {n}: {c.attrs}")
            filtered.append(c)
        except:
            notRunning.append(n)

    for c in filtered:
        print_container(c)

    for n in notRunning:
        print(f"Course: {n} (Not Running)")


def prompt_port(message: str, default: int) -> int:
    value = typer.prompt(
        message,
        default=default,
        type=int,
    )

    while value < 0 or value > 65535:
        typer.secho(
            f"`{value}` is not a valid port number.",
            fg=typer.colors.RED,
        )

        value = typer.prompt(
            message,
            default=default,
            type=int,
        )
    return value


def port_mapping(mapping: str) -> Mapping:
    m = re.fullmatch("^(([0-9]{1,5})(?:/(?:tcp|udp))?)(?::([0-9]{1,5}))?$", mapping)
    if not m:
        return {}

    container = m.group(1)
    srcPort = int(m.group(2))
    hostPort = int(m.group(3) or m.group(2))

    if srcPort < 0 or srcPort > 65535 or hostPort < 0 or hostPort > 65535:
        typer.secho(
            "Invalid port number.",
            fg=typer.colors.RED,
        )
        return {}

    return {container: hostPort}


def prompt_custom_ports() -> Mapping:
    value = typer.prompt(
        "Custom ports to expose, in the form of 'container[:host]', or no input to skip ",
        default="",
        value_proc=lambda v: v.strip(),
        type=str,
    )

    mapping = port_mapping(value)
    if value != "" and not mapping:
        typer.secho(
            "Invalid port specification, please try again.",
            fg=typer.colors.RED,
        )

    mappings = mapping

    while value != "":
        value = typer.prompt(
            "Custom ports to expose, in the form of 'container:host', or no input to skip ",
            default="",
            value_proc=lambda v: v.strip(),
            type=str,
        )

        mapping = port_mapping(value)
        if value != "" and not mapping:
            typer.secho(
                "Invalid port specification, please try again.",
                fg=typer.colors.RED,
            )
            continue

        if mapping:
            mappings = {**mappings, **mapping}

    return mappings


@app.command()
def config(
    ctx: typer.Context,
    configFile: Optional[Path] = configOption,
):
    """Setup a new course resource, or edit an existing one"""

    if not configFile:
        typer.echo(
            f"Config file not found, or not specified. Make sure that file exists or use `--config=FILE` to specify the file"
        )
        raise typer.Exit(1)

    config = ctx.default_map
    _LOGGER.info(f"config contents: {config}")

    course_name = typer.prompt("What's the name of the course?")
    safe_course_name = "".join(
        x for x in course_name.replace(" ", "_") if x.isalnum() or x == "_"
    )

    default_image = "scioer/oo-java:W23"
    default_volume = os.path.join(os.path.expanduser("~/Desktop"), safe_course_name)

    course = config.get(safe_course_name, {})

    docker_image = typer.prompt(
        "What docker image does the course use?",
        default=course.get("image", default_image),
    )

    auto_pull = typer.confirm(
        "Automatically fetch new versions", default=course.get("auto_pull", False)
    )

    course_storage = typer.prompt(
        "Where should the files for the course be stored?",
        default=course.get("volume", default_volume),
    )

    useDefaults = typer.confirm("Use the default ports", default=True)

    mappings = {
        "3000": 3000,
        "8888": 8888,
        "8000": 8000,
        "22": 2222,
    }

    if not useDefaults:
        wiki_port = prompt_port(
            "Wiki port to expose, (0 to publish a random port)",
            3000,
        )

        jupyter_port = prompt_port(
            "Jupyter notebooks port to expose, (0 to publish a random port)",
            8888,
        )

        lectures_port = prompt_port(
            "Lectures port to expose, (0 to publish a random port)",
            8000,
        )

        ssh_port = prompt_port(
            "ssh port to expose, (0 to publish a random port)",
            2222,
        )

        customPorts = prompt_custom_ports()

        mappings = {
            "3000": wiki_port,
            "8888": jupyter_port,
            "8000": lectures_port,
            "22": ssh_port,
            **customPorts,
        }

    ports = [f"{k}:{v}" for k, v in mappings.items()]

    config[safe_course_name] = {
        "image": docker_image,
        "volume": os.path.realpath(os.path.expanduser(course_storage)),
        "ports": ports,
        "auto_pull": auto_pull,
        "public": False,
    }

    parser.save_config_file(configFile, config)


@app.callback()
def setup(
    verbose: Optional[bool] = typer.Option(False, "--verbose", "-v"),
    debug: Optional[bool] = typer.Option(False, "--debug"),
    version: Optional[bool] = typer.Option(
        None, "--version", "-V", callback=version_callback, is_eager=True
    ),
):
    level = logging.WARNING
    if debug:
        level = logging.DEBUG
    elif verbose:
        level = logging.INFO

    logging.basicConfig(level=level)


if __name__ == "__main__":
    app()
