#!/usr/bin/env python
#
# NEED POSIX (i.e. *Cygwin* on Windows).
"""
Script to bump, commit and tag new versions.

USAGE:
  bumpver
  bumpver [-n] [-f] [-c] [-a] [-t <message>]  <new-ver>

Without <new-ver> prints version extracted from current file.
Don't add a 'v' prefix!

OPTIONS:
  -a, --amend       Amend current commit for setting the "chore(ver): ..." msg.
  -f, --force       Bump (and optionally) commit/tag if version exists/is same.
  -n, --dry-run     Do not write files - just pretend.
  -c, --commit      Commit afterwardswith a commit-message describing version bump.
  -t, --tag=<msg>   Adds a signed tag with the given message (commit implied).


- Pre-releases: when working on some version
    X.YbN               # Beta release
    X.YrcN  or  X.YcN   # Release Candidate
    X.Y                 # Final release
- Post-release:
    X.YaN.postM         # Post-release of an alpha release
    X.YrcN.postM        # Post-release of a release candidate
- Dev-release:
    X.YaN.devM          # Developmental release of an alpha release
    X.Y.postN.devM      # Developmental release of a post-release

EXAMPLE:
    bumpver -t 'Mostly model changes' 1.6.2b0

"""

import functools as fnt
import os.path as osp
import re
import sys
from datetime import datetime

import docopt

my_dir = osp.dirname(__file__)

VFILE = osp.join(my_dir, "..", "pypiserver", "__init__.py")
VFILE_regex_version = re.compile(r'version *= *__version__ *= *"([^"]+)"')
VFILE_regex_datetime = re.compile(r'__updated__ *= *"([^"]+)"')
VFILE_regex_date = re.compile(r'__updated__ *= *"([^"\s]+)\s')

RFILE = osp.join(my_dir, "..", "README.md")

PYTEST_ARGS = [osp.join("tests", "test_docs.py")]


class CmdException(Exception):
    pass


def get_current_date_info() -> (str, str):
    now = datetime.now()
    new_datetime = now.strftime("%Y-%m-%d %H:%M:%S%z")
    new_date = now.strftime("%Y-%m-%d")
    return (new_datetime, new_date)


@fnt.lru_cache()
def read_txtfile(fpath):
    with open(fpath, "rt", encoding="utf-8") as fp:
        return fp.read()


def extract_file_regexes(fpath, regexes):
    """
    :param regexes:
        A sequence of regexes to "search", having a single capturing-group.
    :return:
        One groups per regex, or raise if any regex did not match.
    """
    txt = read_txtfile(fpath)
    matches = [regex.search(txt) for regex in regexes]

    if not all(matches):
        raise CmdException(
            "Failed extracting current versions with: %s"
            "\n  matches: %s" % (regexes, matches)
        )

    return [m.group(1) for m in matches]


def replace_substrings(files, subst_pairs):
    for fpath in files:
        txt = read_txtfile(fpath)

        replacements = []
        for old, new in subst_pairs:
            replacements.append((old, new, txt.count(old)))
            txt = txt.replace(old, new)

        yield (txt, fpath, replacements)


def format_syscmd(cmd):
    if isinstance(cmd, (list, tuple)):
        cmd = " ".join('"%s"' % s if " " in s else s for s in cmd)
    else:
        assert isinstance(cmd, str), cmd

    return cmd


def strip_ver2_commonprefix(ver1, ver2):
    cprefix = osp.commonprefix([ver1, ver2])
    if cprefix:
        striplen = cprefix.rfind(".")
        if striplen > 0:
            striplen += 1
        else:
            striplen = len(cprefix)
        ver2 = ver2[striplen:]

    return ver2


def run_testcases():
    import pytest

    retcode = pytest.main(PYTEST_ARGS)
    if retcode:
        raise CmdException(
            "Doc TCs failed(%s), probably version-bumping has failed!" % retcode
        )


def exec_cmd(cmd):
    import subprocess as sbp

    err = sbp.call(cmd, stderr=sbp.STDOUT)
    if err:
        raise CmdException("Failed(%i) on: %s" % (err, format_syscmd(cmd)))


def do_commit(new_ver, old_ver, dry_run, amend, ver_files):
    import pathlib

    cmt_msg = "chore(ver): bump %s-->%s" % (old_ver, new_ver)

    ver_files = [pathlib.Path(f).as_posix() for f in ver_files]
    git_add = ["git", "add"] + ver_files
    git_cmt = ["git", "commit", "-m", cmt_msg]
    if amend:
        git_cmt.append("--amend")
    commands = [git_add, git_cmt]

    for cmd in commands:
        cmd_str = format_syscmd(cmd)
        if dry_run:
            yield "DRYRUN: %s" % cmd_str
        else:
            yield "EXEC: %s" % cmd_str
            exec_cmd(cmd)


def do_tag(tag, tag_msg, dry_run, force):
    cmd = ["git", "tag", tag, "-s", "-m", tag_msg]
    if force:
        cmd.append("--force")
    cmd_str = format_syscmd(cmd)
    if dry_run:
        yield "DRYRUN: %s" % cmd_str
    else:
        yield "EXEC: %s" % cmd_str
        exec_cmd(cmd)


def bumpver(
    new_ver, dry_run=False, force=False, amend=False, tag_name_or_commit=None
):
    """
    :param tag_name_or_commit:
        if true, do `git commit`, if string, also `git tag` with that as msg.
    """
    if amend:
        ## Restore previous version before extracting it.
        cmd = "git checkout HEAD~ --".split()
        cmd.append(VFILE)
        cmd.append(RFILE)
        exec_cmd(cmd)

    regexes = [VFILE_regex_version, VFILE_regex_datetime, VFILE_regex_date]
    old_ver, old_datetime, old_date = extract_file_regexes(VFILE, regexes)

    if not new_ver:
        yield old_ver
        yield old_datetime
        yield old_date
    else:
        if new_ver == old_ver:
            msg = "Version '%s'already bumped"
            if force:
                msg += ", but --force  effected."
                yield msg % new_ver
            else:
                msg += "!\n Use of --force recommended."
                raise CmdException(msg % new_ver)

        new_datetime, new_date = get_current_date_info()
        ver_files = [osp.normpath(f) for f in [VFILE, RFILE]]
        subst_pairs = [
            (old_ver, new_ver),
            (old_datetime, new_datetime),
            (old_date, new_date),
        ]

        for repl in replace_substrings(ver_files, subst_pairs):
            new_txt, fpath, replacements = repl

            if not dry_run:
                with open(fpath, "wt", encoding="utf-8") as fp:
                    fp.write(new_txt)

            yield "%s: " % fpath
            for old, new, nrepl in replacements:
                yield "  %i x (%24s --> %s)" % (nrepl, old, new)

        yield "...now launching DocTCs..."
        run_testcases()

        if tag_name_or_commit is not None:
            yield from do_commit(new_ver, old_ver, dry_run, amend, ver_files)

            if isinstance(tag_name_or_commit, str):
                tag = "v%s" % new_ver
                yield from do_tag(tag, tag_name_or_commit, dry_run, force)


def main(*args):
    opts = docopt.docopt(__doc__, argv=args)

    new_ver = opts["<new-ver>"]

    assert not new_ver or new_ver[0] != "v", (
        "Version '%s' must NOT start with `v`!" % new_ver
    )

    commit = opts["--commit"]
    tag = opts["--tag"]
    if tag:
        tag_name_or_commit = tag
    elif commit:
        tag_name_or_commit = True
    else:
        tag_name_or_commit = None

    try:
        for i in bumpver(
            new_ver,
            opts["--dry-run"],
            opts["--force"],
            opts["--amend"],
            tag_name_or_commit,
        ):
            print(i)
    except CmdException as ex:
        sys.exit(str(ex))
    except Exception as ex:
        print("Unexpected error happened.")
        raise ex


if __name__ == "__main__":
    main(*sys.argv[1:])
