#!/usr/bin/env python3

# Sorts and canonicalizes the Metrics Timeline Markdown.

import datetime
import getopt
import os
import os.path
import re
import sys
import tempfile
import urllib.parse

import parser

def usage(file=sys.stdout):
    print(f"""\
Usage: {sys.argv[0]} [FILENAME]...

Sorts and canonicalizes the Metrics Timeline Markdown. Leaves
everything except the table unchanged.

  -h, --help      show this help
  -i, --in-place  rewrite input files in place
""", file=file)

def format_datetime(d):
    if d is None:
        return None
    if isinstance(d, datetime.datetime):
        return d.strftime("%Y-%m-%d %H:%M:%S")
    else:
        return d.strftime("%Y-%m-%d")

def format_datetime_approx(d, approx):
    if d is None:
        return ""
    result = format_datetime(d)
    if approx:
        result = "~" + result
    return result

# Canonicalize None/date/datetime values to use as a sort key.
def date_for_sort(d):
    if d is None:
        return datetime.datetime.utcfromtimestamp(0)
    if isinstance(d, datetime.datetime):
        return d
    else:
        # When we only know the date, set the time to 12:00.
        return datetime.datetime.combine(d, datetime.time(12))

def tidy_url(url):
    components = urllib.parse.urlparse(url)
    # Rewrite ticket links into permalink form.
    if components.netloc == "trac.torproject.org":
        m = re.match(r'^/projects/tor/ticket/(\d+)', components.path)
        if m:
            components = components._replace(scheme="https", netloc="bugs.torproject.org", path="/"+m.group(1))
    elif components.netloc == "gitlab.torproject.org":
        m = re.match(r'^/(.+)/-/issues/(\d+)', components.path)
        if m:
            components = components._replace(scheme="https", netloc="bugs.torproject.org", path="/"+m.group(1)+"/"+m.group(2))

    # The metrics group was moved into the network-health group in June 2021.
    # https://lists.torproject.org/pipermail/tor-project/2021-June/003130.html
    if components.netloc in ("gitlab.torproject.org", "bugs.torproject.org"):
        m = re.match(r'^/tpo/metrics/(.*)', components.path)
        if m:
            components = components._replace(path="/tpo/network-health/metrics/"+m.group(1))

    return components.geturl()

def tidy_links(node):
    if isinstance(node, parser.Markdown):
        node.children = [tidy_links(child) for child in node.children]
    elif isinstance(node, parser.MarkdownLink):
        node.href = tidy_url(node.href)
    return node

def tidy_entry(entry):
    if entry.end_date is not None:
        # Ensure there is no end date without a start date.
        if entry.start_date is None:
            raise ValueError(f"end date {format_datetime_approx(entry.end_date, entry.end_date_is_approx)!r} without start date")
        # Ensure that start date precedes end date.
        if entry.end_date.timetuple() < entry.start_date.timetuple():
            raise ValueError(f"start date {format_datetime_approx(entry.start_date, entry.start_date_is_approx)!r} is not before end date {format_datetime_approx(entry.end_date, entry.end_date_is_approx)!r}")
    entry.description = tidy_links(entry.description)
    entry.links = [tidy_links(link) for link in entry.links]
    return entry

def write_table_header(file, cells):
    write_table_row(file, (cell.ljust(3) for cell in cells))
    write_table_row(file, ("-"*max(3, len(cell)) for cell in cells))

def write_table_row(file, cells):
    print("|" + "|".join(parser.backslash_escape(cell, "|") for cell in cells) + "|", file=file)

def entry_to_cells(entry):
	return (
		format_datetime_approx(entry.start_date, entry.start_date_is_approx),
		entry.is_ongoing and "ongoing" or format_datetime_approx(entry.end_date, entry.end_date_is_approx),
		" ".join(sorted(entry.places)),
		" ".join(sorted(entry.protocols)),
		entry.description.to_markdown().strip(),
		" ".join(link.to_markdown().strip() for link in entry.links),
		"X" if entry.is_unknown else "",
	)

def process(f, out_f):
    for x in parser.parse(r):
        if isinstance(x, str):
            # Literal text between tables, output it verbatim.
            out_f.write(x)
            continue

        entries = [tidy_entry(entry) for entry in x]
        # Sort in reverse order by date. For entries with identical dates, sort
        # by places and protocols.
        entries.sort(key=lambda entry: (sorted(entry.places), sorted(entry.protocols)))
        entries.sort(key=lambda entry: (date_for_sort(entry.start_date), date_for_sort(entry.end_date)), reverse=True)

        write_table_header(out_f, parser.EXPECTED_COLUMN_NAMES)
        for entry in entries:
            write_table_row(out_f, entry_to_cells(entry))

IN_PLACE = False

opts, filenames = getopt.gnu_getopt(sys.argv[1:], "hi", ["help", "in-place"])
for o, a in opts:
    if o == "-h" or o == "--help":
        usage()
        sys.exit()
    elif o == "-i" or o == "--in-place":
        IN_PLACE = True

if IN_PLACE:
    for filename in filenames:
        with open(filename) as r:
            with tempfile.NamedTemporaryFile(mode="w", dir=os.path.dirname(filename), delete=False) as w:
                try:
                    process(r, w)
                except:
                    os.remove(w.name)
                    raise
                else:
                    os.rename(w.name, filename)
else:
    if not filenames:
        process(sys.stdin, sys.stdout)
    else:
        for filename in filenames:
            with open(filename) as r:
                process(r, sys.stdout)
