from __future__ import annotations

import functools
import logging
import shutil
import tempfile
from os.path import isdir, isfile, join
from typing import Optional
from urllib.parse import urlsplit

import jinja2.exceptions

from mkdocs.commands.build import build
from mkdocs.config import load_config
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import Abort
from mkdocs.livereload import LiveReloadServer

log = logging.getLogger(__name__)


def serve(
    config_file=None,
    dev_addr=None,
    strict=None,
    theme=None,
    theme_dir=None,
    livereload='livereload',
    watch_theme=False,
    watch=[],
    **kwargs,
):
    """
    Start the MkDocs development server

    By default it will serve the documentation on http://localhost:8000/ and
    it will rebuild the documentation and refresh the page automatically
    whenever a file is edited.
    """
    # Create a temporary build directory, and set some options to serve it
    # PY2 returns a byte string by default. The Unicode prefix ensures a Unicode
    # string is returned. And it makes MkDocs temp dirs easier to identify.
    site_dir = tempfile.mkdtemp(prefix='mkdocs_')

    def mount_path(config: MkDocsConfig):
        return urlsplit(config.site_url or '/').path

    get_config = functools.partial(
        load_config,
        config_file=config_file,
        dev_addr=dev_addr,
        strict=strict,
        theme=theme,
        theme_dir=theme_dir,
        site_dir=site_dir,
        **kwargs,
    )

    live_server = livereload in ('dirty', 'livereload')
    dirty = livereload == 'dirty'

    def builder(config: Optional[MkDocsConfig] = None):
        log.info("Building documentation...")
        if config is None:
            config = get_config()

        # combine CLI watch arguments with config file values
        if config.watch is None:
            config.watch = watch
        else:
            config.watch.extend(watch)

        # Override a few config settings after validation
        config.site_url = f'http://{config.dev_addr}{mount_path(config)}'

        build(config, live_server=live_server, dirty=dirty)

    config = get_config()
    config['plugins'].run_event('startup', command='serve', dirty=dirty)

    try:
        # Perform the initial build
        builder(config)

        host, port = config.dev_addr
        server = LiveReloadServer(
            builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path(config)
        )

        def error_handler(code) -> Optional[bytes]:
            if code in (404, 500):
                error_page = join(site_dir, f'{code}.html')
                if isfile(error_page):
                    with open(error_page, 'rb') as f:
                        return f.read()
            return None

        server.error_handler = error_handler

        if live_server:
            # Watch the documentation files, the config file and the theme files.
            server.watch(config.docs_dir)
            server.watch(config.config_file_path)

            if watch_theme:
                for d in config.theme.dirs:
                    server.watch(d)

            # Run `serve` plugin events.
            server = config.plugins.run_event('serve', server, config=config, builder=builder)

            for item in config.watch:
                server.watch(item)

        try:
            server.serve()
        except KeyboardInterrupt:
            log.info("Shutting down...")
        finally:
            server.shutdown()
    except jinja2.exceptions.TemplateError:
        # This is a subclass of OSError, but shouldn't be suppressed.
        raise
    except OSError as e:  # pragma: no cover
        # Avoid ugly, unhelpful traceback
        raise Abort(f'{type(e).__name__}: {e}')
    finally:
        config['plugins'].run_event('shutdown')
        if isdir(site_dir):
            shutil.rmtree(site_dir)
