# coding: utf-8
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import itertools
import logging
import os
import sys
import tempfile
import time
from argparse import ArgumentParser
from collections import OrderedDict
from datetime import datetime

import pebble.concurrent
import pkg_resources
from functools import partial
from termcolor import colored

from .exceptions import AocdError
from .models import AOCD_CONFIG_DIR
from .models import _load_users
from .models import Puzzle
from .utils import AOC_TZ
from .utils import _cli_guess


# from https://adventofcode.com/about
# every problem has a solution that completes in at most 15 seconds on ten-year-old hardware


DEFAULT_TIMEOUT = 60
log = logging.getLogger(__name__)


def main():
    entry_points = pkg_resources.iter_entry_points(group="adventofcode.user")
    plugins = OrderedDict([(ep.name, ep) for ep in entry_points])
    aoc_now = datetime.now(tz=AOC_TZ)
    years = range(2015, aoc_now.year + int(aoc_now.month == 12))
    days = range(1, 26)
    users = _load_users()
    log_levels = "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
    parser = ArgumentParser(description="AoC runner")
    parser.add_argument("-p", "--plugins", nargs="+", choices=plugins)
    parser.add_argument("-y", "--years", type=int, nargs="+", choices=years)
    parser.add_argument("-d", "--days", type=int, nargs="+", choices=days)
    parser.add_argument("-u", "--users", nargs="+", choices=users, type=partial(_cli_guess, choices=users))
    parser.add_argument("-t", "--timeout", type=int, default=DEFAULT_TIMEOUT)
    parser.add_argument("-s", "--no-submit", action="store_true", help="disable autosubmit")
    parser.add_argument("-r", "--reopen", action="store_true", help="open browser on NEW solves")
    parser.add_argument("-q", "--quiet", action="store_true", help="capture output from runner")
    parser.add_argument("--log-level", default="WARNING", choices=log_levels)
    args = parser.parse_args()

    if not users:
        path = os.path.join(AOCD_CONFIG_DIR, "tokens.json")
        print(
            "There are no datasets available to use.\n"
            "Either export your AOC_SESSION or put some auth "
            "tokens into {}".format(path),
            file=sys.stderr,
        )
        sys.exit(1)
    if not plugins:
        print(
            "There are no plugins available. Install some package(s) with a registered 'adventofcode.user' entry-point.\n"
            "See https://github.com/wimglenn/advent-of-code-sample for an example plugin package structure.",
            file=sys.stderr,
        )
        sys.exit(1)
    logging.basicConfig(level=getattr(logging, args.log_level))
    rc = run_for(
        plugins=args.plugins or list(plugins),
        years=args.years or years,
        days=args.days or days,
        datasets={k: users[k] for k in (args.users or users)},
        timeout=args.timeout,
        autosubmit=not args.no_submit,
        reopen=args.reopen,
        capture=args.quiet,
    )
    sys.exit(rc)


def _timeout_wrapper(f, capture=False, timeout=DEFAULT_TIMEOUT, *args, **kwargs):
    func = pebble.concurrent.process(daemon=False, timeout=timeout)(_process_wrapper)
    return func(f, capture, *args, **kwargs)


def _process_wrapper(f, capture=False, *args, **kwargs):
    # allows to run f in a process which can be killed if it misbehaves
    prev_stdout = sys.stdout
    prev_stderr = sys.stderr
    if capture:
        hush = open(os.devnull, "w")
        sys.stdout = sys.stderr = hush
    try:
        result = f(*args, **kwargs)
    finally:
        if capture:
            sys.stdout = prev_stdout
            sys.stderr = prev_stderr
            hush.close()
    return result


def run_with_timeout(entry_point, timeout, progress, dt=0.1, capture=False, **kwargs):
    spinner = itertools.cycle(r"\|/-")
    line = elapsed = format_time(0)
    t0 = time.time()
    func = entry_point.load()
    future = _timeout_wrapper(func, capture=capture, timeout=timeout, **kwargs)
    while not future.done():
        if progress is not None:
            line = "\r" + elapsed + "   " + progress + "   " + next(spinner)
            sys.stderr.write(line)
            sys.stderr.flush()
        time.sleep(dt)
        elapsed = format_time(time.time() - t0, timeout)
    walltime = time.time() - t0
    try:
        a, b = future.result()
    except Exception as err:
        a = b = ""
        error = repr(err)[:50]
    else:
        error = ""
        # longest correct answer seen so far has been 32 chars
        a = str(a)[:50]
        b = str(b)[:50]
    if progress is not None:
        sys.stderr.write("\r" + " " * len(line) + "\r")
        sys.stderr.flush()
    return a, b, walltime, error


def format_time(t, timeout=DEFAULT_TIMEOUT):
    if t < timeout / 4:
        color = "green"
    elif t < timeout / 2:
        color = "yellow"
    else:
        color = "red"
    runtime = colored("{: 7.2f}s".format(t), color)
    return runtime


def run_one(year, day, input_data, entry_point, timeout=DEFAULT_TIMEOUT, progress=None, capture=False):
    prev = os.getcwd()
    scratch = tempfile.mkdtemp(prefix="{}-{:02d}-".format(year, day))
    os.chdir(scratch)
    assert not os.path.exists("input.txt")
    try:
        with open("input.txt", "w") as f:
            f.write(input_data)
        a, b, walltime, error = run_with_timeout(
            entry_point=entry_point,
            timeout=timeout,
            year=year,
            day=day,
            data=input_data,
            progress=progress,
            capture=capture,
        )
    finally:
        os.unlink("input.txt")
        os.chdir(prev)
        try:
            os.rmdir(scratch)
        except Exception as err:
            log.warning("failed to remove scratch %s (%s: %s)", scratch, type(err), err)
    return a, b, walltime, error


def run_for(plugins, years, days, datasets, timeout=DEFAULT_TIMEOUT, autosubmit=True, reopen=False, capture=False):
    aoc_now = datetime.now(tz=AOC_TZ)
    all_entry_points = pkg_resources.iter_entry_points(group="adventofcode.user")
    entry_points = {ep.name: ep for ep in all_entry_points if ep.name in plugins}
    it = itertools.product(years, days, plugins, datasets)
    userpad = 3
    datasetpad = 8
    n_incorrect = 0
    if entry_points:
        userpad = len(max(entry_points, key=len))
    if datasets:
        datasetpad = len(max(datasets, key=len))
    for year, day, plugin, dataset in it:
        if year == aoc_now.year and day > aoc_now.day:
            continue
        token = datasets[dataset]
        entry_point = entry_points[plugin]
        os.environ["AOC_SESSION"] = token
        puzzle = Puzzle(year=year, day=day)
        title = puzzle.title
        progress = "{}/{:<2d} - {:<40}   {:>%d}/{:<%d}"
        progress %= (userpad, datasetpad)
        progress = progress.format(year, day, title, plugin, dataset)
        a, b, walltime, error = run_one(
            year=year,
            day=day,
            input_data=puzzle.input_data,
            entry_point=entry_point,
            timeout=timeout,
            progress=progress,
            capture=capture,
        )
        runtime = format_time(walltime, timeout)
        line = "   ".join([runtime, progress])
        if error:
            assert a == b == ""
            icon = colored("✖", "red")
            n_incorrect += 1
            line += "   {icon} {error}".format(icon=icon, error=error)
        else:
            result_template = "   {icon} part {part}: {answer}"
            for answer, part in zip((a, b), "ab"):
                if day == 25 and part == "b":
                    # there's no part b on christmas day, skip
                    continue
                expected = None
                try:
                    expected = getattr(puzzle, "answer_" + part)
                except AttributeError:
                    post = part == "a" or (part == "b" and puzzle.answered_a)
                    if autosubmit and post:
                        try:
                            puzzle._submit(answer, part, reopen=reopen, quiet=True)
                        except AocdError as err:
                            log.warning("error submitting - %s", err)
                        try:
                            expected = getattr(puzzle, "answer_" + part)
                        except AttributeError:
                            pass
                correct = expected is not None and str(expected) == answer
                icon = colored("✔", "green") if correct else colored("✖", "red")
                correction = ""
                if not correct:
                    n_incorrect += 1
                    if expected is None:
                        icon = colored("?", "magenta")
                        correction = "(correct answer unknown)"
                    else:
                        correction = "(expected: {})".format(expected)
                answer = "{} {}".format(answer, correction)
                if part == "a":
                    answer = answer.ljust(30)
                line += result_template.format(icon=icon, part=part, answer=answer)
        print(line)
    return n_incorrect
