#!/usr/bin/env python3

# This script e-mails an outstanding MR review request.
#
# Configuration is via environment variables:
#
# In addition to sending the e-mail (or instead of if --to is not given), it
# prints the message body to stdout.
#
# Caveats/gotchas:
#
#   1. The error handling is pretty loose because we can just look at the
#      exception in the CI job log.
#
#   2. 1998 called and wants its HTML back.
#
#   3. The python-gitlab docs [1] have decent examples, but the API reference
#   is nearly useless. Many objects do have a pprint() method that helps. The
#   Gitlab REST API docs [2] do not appear to be comprehensive.
#
# [1]: https://python-gitlab.readthedocs.io/en/stable
# [2]: https://docs.gitlab.com/ee/api


import argparse
import email
import io
import os
import smtplib
import sys

import gitlab  # a.k.a. python-gitlab


### Constants ###

PAGE_LENGTH = 100            # max 100
PROJECT_ID = os.environ.get("CI_PROJECT_ID", "charliecloud/main")

# Review request states appear to be undocumented; there is an example [1] but
# nothing about what the possible values are. There is an open issue
# requesting documentation for this [2], and a comment there points to GraphQL
# docs [3] that list the values below (in all caps).
#
# [1]: https://docs.gitlab.com/ee/api/merge_requests.html#get-single-merge-request-reviewers
# [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/440445
# [3]: https://docs.gitlab.com/ee/api/graphql/reference/#mergerequestreviewstate
RR_STATES_OK = { "approved", "requested_changes", "reviewed" }
RR_STATES_DELINQUENT = { "review_started", "unapproved", "unreviewed" }
RR_STATES_ALL = RR_STATES_DELINQUENT | RR_STATES_OK

### Globals ###

body = io.StringIO()


### Main ###

def main():

   ap = argparse.ArgumentParser(
           formatter_class=argparse.ArgumentDefaultsHelpFormatter)
   ap.add_argument("--to", metavar="ADDR",
                   help="recipient e-mail address")
   ap.add_argument("--from", metavar="ADDR", dest="from_",
                   default="noreply@charliecloud.io",
                   help="sender address")
   ap.add_argument("--host", metavar="HOST",
                   default="localhost",
                   help="SMTP server to use")
   ap.add_argument("--port", metavar="PORT", type=int,
                   default=smtplib.SMTP_PORT,
                   help="TCP port on SMTP server")
   ap.add_argument("--user", metavar="NAME",
                   help="username for SMTP login")
   ap.add_argument("--pass", metavar="WORD", dest="pass_",
                   help="password for SMTP login")
   ap.add_argument("--verbose", "-v", action="store_true",
                   help="log SMTP transaction")
   args = ap.parse_args()
   if ((args.user is None) + (args.pass_ is None) == 1):
      ap.error("both --user and --pass must be given together")

   gl = gitlab.Gitlab("https://gitlab.com", per_page=PAGE_LENGTH)
   pj = gl.projects.get(PROJECT_ID)
   OUT("<!DOCTYPE html>")
   OUT("<html><body>")

   OUT("<p><table>")
   OUT("<tr><td>project:</td><td><a href='%s'>%s</a> %d</td>"
       % (pj.web_url, pj.path_with_namespace, pj.id))

   # analyze projects
   mrs = pj.mergerequests.list(state="opened", get_all=True,
                               order_by="created_at", sort="asc")
   delinquents = dict()
   OUT("<tr><td>MRs open:&nbsp;</td> <td>%d</td>" % len(mrs))
   OUT("<tr><td>job:</td> <td>%s</td></tr>" % os.environ.get("CI_JOB_URL"))
   OUT("</table></p>")
   for (n, mr) in enumerate(mrs, 1):
      revs = mr.reviewer_details.list()
      LOG("%d. MR !%d:" % (n, mr.iid))
      delinquents_p = False
      for rev in revs:
         LOG("%s: %s" % (rev.user["username"], rev.state))
         if (rev.state not in RR_STATES_ALL):
            LOG("bad state: %s" % rev.state)
            sys.exit(1)
         if (rev.state in RR_STATES_DELINQUENT):
            delinquents_p = True
      if (not delinquents_p):
         LOG("no delinquents")
         continue
      # at least one delinquent, so include MR in the e-mail
      OUT()
      OUT("<p>")
      OUT("<strong>%d. MR <a href='%s'>!%d</a>: %s</strong><br/>"
          % (n, mr.web_url, mr.iid, mr.title))
      OUT("<table>")
      for d in mr.reviewer_details.list():
         OUT("<tr> <td><strong>%s:</strong>&nbsp;</td> <td>%s"
             % (d.user["username"], d.state))
         if (d.state not in RR_STATES_DELINQUENT):
            OUT(" ok")
         else:
            OUT(" <font color=red><blink>DELINQUENT</blink></font>")
            delinquents[d.user["username"]] = d.user
         OUT("</td></tr>")
      for i in mr.closes_issues():
         OUT("<tr> <td>closes:</td> <td><a href='%s'>#%d</a>: %s</td> </tr>"
             % (i.web_url, i.iid, i.title))
      OUT("</table></p>")

   # call out delinquents by name
   OUT()
   OUT("<p>")
   OUT("<strong>found %d delinquents:</strong><br/>" % len(delinquents))
   OUT("<table>")
   for u in sorted(delinquents.values(), key=lambda x: x["username"]):
      OUT("<tr> <td>%s&nbsp;</td> <td>%s</td> </tr>"
          % (u["username"], u["name"]))
   OUT("</table></p>")

   OUT("</body></html>")

   # send e-mail
   LOG()
   if (not args.to):
      LOG("--to not given, skipping e-mail")
   elif (len(delinquents) == 0):
      LOG("no delinquents, skipping e-mail")
   else:
      LOG("sending e-mail to %s ..." % args.to)
      msg = email.message.EmailMessage()
      # Use a boring subject line rather than the fancy one because the latter
      # is silently not delivered by at least one provider, I guess because it
      # looks too spammy?
      msg["Subject"] = "delinquent reviews report"
      #msg["Subject"] = "🤯🤯🤯 You WON’T BELIEVE who is DeLiNqUeNt!! CLICK HERE to find out!!! 🤯🤯🤯"
      msg["From"] = args.from_
      msg["To"] = args.to
      msg.set_content("no plain text, only HTML, sorry")
      msg.add_alternative(body.getvalue(), subtype="html")
      smtp = smtplib.SMTP(args.host) # https://github.com/python/cpython/issues/80275
      if (args.verbose):
         smtp.set_debuglevel(1)
      LOG("SMTP: connecting to %s:%s" % (args.host, args.port))
      smtp.connect(args.host, args.port)
      smtp.ehlo()
      smtp.starttls()
      smtp.ehlo()
      if (args.user):
         smtp.login(args.user, args.pass_)
      refused = smtp.send_message(msg)
      if (len(refused) > 0):
         LOG("SMTP: %d recipients refused: %s" % (len(refused), refused))
      quit = smtp.quit()
      LOG("SMTP: session closed: %d %s" % (quit[0], quit[1].decode("utf8")))

   LOG("done")



### Functions ###

def LOG(*args, **kwargs):
   print(*args, **kwargs, file=sys.stderr)

def OUT(*args, **kwargs):
   print(*args, **kwargs, file=sys.stdout)
   print(*args, **kwargs, file=body)

### Bootstrap ###

if (__name__ == "__main__"):
   main()
