"""
CodeTrack v. 0.99.3 - A tool to track software defects, bug reports, and
change requests.

Python port of the original PHP application.  Uses Flask as the web
framework and stores data in simple XML text files (no database required).

See http://www.openbugs.org/ for the original CodeTrack.

This software is released under the GPL and is copyrighted by Kenneth White,
2001-2006.  Complete license is LICENSE.txt in the docs directory.

local rename is esltrack
"""

import os
import re
import time
import base64
import hashlib
import pickle
import shutil
import random
import string
import urllib.parse
from datetime import datetime, timedelta

from flask import (
    Flask, request, redirect, make_response, Response, render_template_string
)

import config as CT

app = Flask(__name__)

# ---------------------------------------------------------------------------
# Global state (mirrors PHP globals)
# ---------------------------------------------------------------------------
xml_array_g = []
current_session_g = {}
debug_g = False


# ===========================================================================
#  Utility helpers
# ===========================================================================

def no_cache():
    """Emit no-cache headers (PHP no_cache())."""
    # Handled via response headers in Flask; see helper below.
    pass


def _no_cache_response(resp):
    resp.headers["Expires"] = "Mon, 26 Jul 1997 05:00:00 GMT"
    resp.headers["Last-Modified"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
    resp.headers["Cache-Control"] = "no-cache, must-revalidate"
    resp.headers["Pragma"] = "no-cache"
    return resp


def calc_session_id(user_full_name):
    """Create a one-day session ID (MD5 of date + name + private key)."""
    if not CT.CT_PRIVATE_KEY:
        raise RuntimeError("Fatal: Internal - no authentication key!")
    if not user_full_name:
        return False
    todays_sid = datetime.now().strftime("%d%b%Y") + user_full_name + CT.CT_PRIVATE_KEY
    return hashlib.md5(todays_sid.encode("utf-8")).hexdigest()


def build_new_session_cookie(user_data, selected_project):
    if not user_data or not selected_project:
        raise RuntimeError("Fatal-Internal: No User Data or Selected Project!")

    session_auth = calc_session_id(user_data["Full_Name"])

    session = {
        "user_full_name": user_data["Full_Name"],
        "id": session_auth,
        "project": selected_project,
        "username": user_data["Username"],
        "role": user_data["Role"],
    }
    serialized = urllib.parse.quote(base64.b64encode(pickle.dumps(session)).decode("utf-8"))
    return serialized


def build_updated_session_cookie(project_name, current_session):
    session = {
        "user_full_name": current_session["user_full_name"],
        "id": current_session["id"],
        "project": project_name,
        "username": current_session["username"],
        "role": current_session["role"],
    }
    serialized = urllib.parse.quote(base64.b64encode(pickle.dumps(session)).decode("utf-8"))
    return serialized


def decode_session_cookie(cookie):
    try:
        return pickle.loads(base64.b64decode(urllib.parse.unquote(cookie)))
    except Exception:
        return None


def set_session_cookie_load_page(serialized_session, page_to_load):
    """Set session cookie and redirect (PHP used JS; we use a redirect)."""
    resp = make_response(redirect(f"/esltrack.php?page={page_to_load}"))
    resp.set_cookie("esltrack_session", serialized_session, path="/")
    return resp


def calc_next_node_id(node_list):
    max_id = 0
    for node in node_list:
        if int(node.get("ID", 0)) > max_id:
            max_id = int(node["ID"])
    return max_id + 1


def get_array_index_for_id(data_array, id_):
    row_cnt = len(data_array)
    if row_cnt == 1:
        return 0
    for index in range(row_cnt):
        if str(data_array[index].get("ID", "")) == str(id_):
            return index
    raise RuntimeError(f"Fatal: No issue matching ID# {id_} found!")


def get_next_bug_id(bug_array, current_row_id):
    n = current_row_id
    next_id = None
    while True:
        n += 1
        if n >= len(bug_array):
            break
        if ("Delete_Bug" not in bug_array[n]
                and bug_array[n].get("Project") == current_session_g.get("project")
                and "Closed" not in bug_array[n].get("Status", "")):
            next_id = bug_array[n]["ID"]
            break
    if next_id is None:
        next_id = bug_array[current_row_id]["ID"]
    return next_id


def get_prev_bug_id(bug_array, current_row_id):
    p = current_row_id
    prev_id = None
    while True:
        p -= 1
        if p < 0:
            break
        if ("Delete_Bug" not in bug_array[p]
                and bug_array[p].get("Project") == current_session_g.get("project")
                and "Closed" not in bug_array[p].get("Status", "")):
            prev_id = bug_array[p]["ID"]
            break
    if prev_id is None:
        prev_id = bug_array[current_row_id]["ID"]
    return prev_id


def authorized_user(project_id, user_id, permission_table):
    for permission in permission_table:
        if (permission.get("Project_ID") == str(project_id)
                and permission.get("User_ID") == str(user_id)):
            return True
    return False


def admin_check():
    if current_session_g.get("role") != "Admin":
        raise RuntimeError("<br /><br /><br /><center><h2>You are not an administrator.")


def get_stats(time_in_seconds):
    days = time_in_seconds / 86400.0
    if days:
        return "%3.1f days" % days
    return "(n/a)"


def parse_human_date(date_string):
    try:
        dt = datetime.strptime(date_string, CT.CT_DATE_FORMAT)
    except Exception:
        return date_string
    today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
    if dt > today:
        return "Today, " + dt.strftime("%I:%M %p")
    if dt > today - timedelta(days=1):
        return "Yesterday, " + dt.strftime("%I:%M %p")
    return date_string


def reverse_htmlspecialchars(html_encoded_string):
    """Undo htmlspecialchars: &quot; -> \" etc."""
    trans = {
        "&quot;": '"', "&#039;": "'", "&lt;": "<", "&gt;": ">", "&amp;": "&",
    }
    for k, v in trans.items():
        html_encoded_string = html_encoded_string.replace(k, v)
    return html_encoded_string


def encrypt_password(plaintext_password):
    """Produce an alphanumeric-only MD5-based hash (PHP crypt() equivalent)."""
    sanity_check = 0
    while True:
        salt = "".join(chr(random.randint(48, 122)) for _ in range(2))
        # Emulate PHP crypt() with a 2-char salt -> use MD5-based crypt
        import crypt as _crypt
        encrypted = _crypt.crypt(plaintext_password, salt)
        if re.search(r"[^0-9a-z]", encrypted, re.I) is None:
            return encrypted
        sanity_check += 1
        if sanity_check > 250:
            raise RuntimeError("Fatal: Could not construct valid encrypted password!")


def valid_password(login_data, user_table):
    """Return (match, index)."""
    user_cnt = len(user_table)
    match = False
    index = -1
    for i in range(user_cnt):
        if login_data.get("username") == user_table[i].get("Username"):
            plaintext = login_data.get("password", "")
            hashed = user_table[i].get("Password", "")
            salt = hashed[:2]
            import crypt as _crypt
            match = (_crypt.crypt(plaintext, salt) == hashed)
            index = i
            break
    return match, index


# ===========================================================================
#  File locking (brute-force lock files)
# ===========================================================================

def brute_force_lock(filename):
    brute_force_lock_check(filename)
    lockfile = filename + ".lck"
    try:
        open(lockfile, "a").close()
    except OSError:
        raise RuntimeError(
            f"<br /><br /><center><h3>Fatal: Could not create file lock {lockfile} ! "
            "<br /> Make sure the web server has write permissions in this directory")


def brute_force_lock_check(filename):
    lockfile = filename + ".lck"
    timeout = 0
    while os.path.exists(lockfile):
        time.sleep(1)
        if timeout > CT.CT_LOCKFILE_TIMEOUT:
            brute_force_lock_release(filename)
            raise RuntimeError(
                f"<br /><br /><br /><center><h3>Fatal: Timed out waiting on file lock {lockfile} !"
                "<br />Lock has been cleared.<br />Please report this message to support.")
        timeout += 1


def brute_force_lock_release(filename):
    lockfile = filename + ".lck"
    try:
        os.unlink(lockfile)
    except OSError:
        raise RuntimeError(
            f"<br /><center>Fatal: Could not release file lock {lockfile} ! "
            "Please report this message to support.")


# ===========================================================================
#  XML parsing / writing
# ===========================================================================

def parse_xml_file(xml_filename, drop_history=False, sort_element="", sort_order=""):
    """Non-validating single-pass XML parser.  Populates global xml_array_g."""
    global xml_array_g
    xml_array_g = []

    try:
        with open(xml_filename, "r", encoding="utf-8", errors="replace") as fp:
            xml_document = fp.read()
    except OSError:
        raise RuntimeError(
            f"<br /><br /><center><h2>Fatal: Error opening '{xml_filename}'. "
            "Make sure it's readable by the web server owner.")

    # Pull out <tag_name>(data)</tag_name>
    matches = re.findall(r"<([\w]+)[^>]*>([^<]*)</\1>", xml_document)

    node_cnt = 0
    node_id = 0
    index = 0

    for tag, content in matches:
        if tag == "ID":
            node_id = int(content) if content.isdigit() else 0
            if drop_history:
                index = node_id
                if node_id < len(xml_array_g):
                    xml_array_g[node_id] = {}
                else:
                    while len(xml_array_g) <= node_id:
                        xml_array_g.append({})
            else:
                index = node_cnt
                xml_array_g.append({})
            node_cnt += 1
        if index < len(xml_array_g):
            xml_array_g[index][tag] = content

    # Remove empty dicts left over from drop_history gaps
    xml_array_g = [x for x in xml_array_g if x]

    if sort_element:
        if len(xml_array_g) == 1:
            pass
        elif sort_order == CT.CT_ASCENDING:
            xml_array_g.sort(key=lambda x: x.get(sort_element, "").lower())
        else:
            xml_array_g.sort(key=lambda x: x.get(sort_element, "").lower(), reverse=True)


def parse_dtd_file(filename):
    try:
        with open(filename, "r", encoding="utf-8", errors="replace") as fh:
            dtd = fh.read()
    except OSError:
        raise RuntimeError(
            f"<br /><center><h3>Fatal: Couldn't open '{filename}' ! "
            "Check that it's readable by the web server owner.")
    matches = re.findall(r"<!ELEMENT\s+(\w+)\s+\(\s*#PCDATA\s*\)>", dtd)
    return matches


def append_xml_file_node(filename, node, closing_root_tag):
    brute_force_lock(filename)

    try:
        with open(filename, "rb+") as fh:
            fh.seek(0, os.SEEK_END)
            fsize = fh.tell()
            fh.seek(-CT.CT_LAST_LINE_SIZE, os.SEEK_END)
            last_line = fh.read(CT.CT_LAST_LINE_SIZE).decode("utf-8", errors="replace")
    except OSError:
        raise RuntimeError(
            f"<br /><center><h3>Fatal: Couldn't append to '{filename}' ! "
            "Check that it's writable by the web server owner.")

    pos = last_line.rfind(closing_root_tag)
    offset = CT.CT_LAST_LINE_SIZE - pos if pos != -1 else 0
    append_location = fsize - offset

    node += "\n" + closing_root_tag

    # Translate bare \n to \r\n for friendly transfers
    node = re.sub(r"([^\r])\n", r"\1\r\n", node)

    try:
        with open(filename, "rb+") as fh:
            fh.seek(append_location)
            fh.write(node.encode("utf-8"))
    except OSError:
        raise RuntimeError(f"Fatal: Unable to add XML node to '{filename}' !")

    brute_force_lock_release(filename)


def rewrite_xml_file(filename, parsed_tree, major_element_tag):
    root_element_tag = major_element_tag + "s"

    brute_force_lock(filename)

    opening_content = (
        '<?xml version="1.0" ?>\r\n'
        f"<!--\t{root_element_tag.capitalize()} database for esltrack.  Edit at your own risk.    -->\r\n"
        "<!--\tMeaning: re-check for XML well-formedness and validity!  -->\r\n"
        f'<!DOCTYPE {root_element_tag} SYSTEM "{root_element_tag}.dtd">\r\n'
        f"<{root_element_tag}>\r\n"
    )

    try:
        with open(filename, "wb") as fh:
            fh.write(opening_content.encode("utf-8"))
            for node in parsed_tree:
                fh.write(f"\t<{major_element_tag}>\r\n".encode("utf-8"))
                for element, contents in node.items():
                    fh.write(f"\t\t<{element}>{contents}</{element}>\r\n".encode("utf-8"))
                fh.write(f"\t</{major_element_tag}>\r\n".encode("utf-8"))
            fh.write(f"</{root_element_tag}>".encode("utf-8"))
    except OSError:
        raise RuntimeError(f"Fatal: Unable to write XML data to '{filename}' !")

    brute_force_lock_release(filename)


def update_xml_file_node(filename, parsed_tree, major_element_tag, update_node):
    node_cnt = len(parsed_tree)
    target_id = update_node["ID"]
    node_updated = False

    for node_index in range(node_cnt):
        if parsed_tree[node_index].get("ID") == target_id:
            for element, contents in update_node.items():
                parsed_tree[node_index][element] = contents
            node_updated = True

    if not node_updated:
        raise RuntimeError(f"<br /><center>Fatal:  XML Node ID {target_id} not found! ")

    rewrite_xml_file(filename, parsed_tree, major_element_tag)


# ===========================================================================
#  Data scrubbing
# ===========================================================================

def scrub_and_tag_form_data(data_array):
    for element in list(data_array.keys()):
        untrusted_content = data_array[element]
        if untrusted_content == "":
            del data_array[element]
        else:
            untrusted_content = str(untrusted_content)[:CT.CT_MAX_DESCR_SIZE].strip()
            content = re.sub(
                r'[^ 0-9a-z_@#$%();:?+*!=&,"\'\n\t.-]+', "", untrusted_content, flags=re.I)
            clean_data = f"<{element}>" + _htmlspecialchars(content.strip()) + f"</{element}>"
            data_array[element] = clean_data
    return data_array


def _htmlspecialchars(s):
    return (s.replace("&", "&amp;").replace('"', "&quot;")
             .replace("'", "&#039;").replace("<", "&lt;").replace(">", "&gt;"))


def process_file_attachment(tmp_filename, requested_filename):
    filename = re.sub(r"( +)", "_", requested_filename.strip())
    filename = re.sub(r"([^.0-9a-z_-]+)", "", filename, flags=re.I)
    filename = re.sub(r"(\.+)", ".", filename)

    if len(filename) > CT.CT_MAX_ATTACHMENT_FILENAME_SIZE:
        raise RuntimeError(
            "\n<br /><br /><center><h3> The filename of your attachment is too long.<br />"
            f"The name cannot exceed {CT.CT_MAX_ATTACHMENT_FILENAME_SIZE} characters.<br /><br />"
            "* Please contact the esltrack admin if this is incorrect. * "
            '<br />( check "CT_MAX_ATTACHMENT_FILENAME_SIZE" setting )')

    legal_attachment_types = [x.strip() for x in CT.CT_LEGAL_ATTACHMENTS.split(",")]
    legal = False
    for legal_type in legal_attachment_types:
        if re.search(r"\.%s$" % re.escape(legal_type), filename, re.I):
            legal = True
            break
    if not legal:
        raise RuntimeError(
            "\n<br /><br /><center><h3> Your attachment is not a permissible file.<br /></h3>"
            f"Legal file types: <tt>{CT.CT_LEGAL_ATTACHMENTS}</tt><br /><br />"
            ' * Please contact the esltrack admin if this is incorrect (check "CT_LEGAL_ATTACHMENTS" setting) *')

    os.makedirs(CT.CT_ATTACHMENTS_DIRECTORY, exist_ok=True)
    dest = os.path.join(CT.CT_ATTACHMENTS_DIRECTORY, filename)
    try:
        shutil.move(tmp_filename, dest)
        os.chmod(dest, 0o600)
        return filename
    except OSError:
        raise RuntimeError(
            f"<br /><b>Fatal: File attachment transfer failed for TMP: {tmp_filename} "
            f"FILE: {filename} </b><br /><center><h3>Please notify support!!!")


# ===========================================================================
#  Drawing / rendering functions
# ===========================================================================

def draw_page_header():
    return f"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Refresh" content="{CT.CT_DEFAULT_PAGE_TIMEOUT};URL=esltrack.php?page=timeout" />
<title>esltrack: Bug and Defect Reporting {CT.CT_CODE_VERSION} </title>
<link rel="stylesheet" href="style/codetrack.css" type="text/css" />
<link rel="stylesheet" href="images/cfgChkAccess.css" type="text/css" />
<link rel="stylesheet" href="javascript/cfgChkAccess.css" type="text/css" />
<link rel="stylesheet" href="style/codetrack_w3c.css" type="text/css" media="all" />
</head>
<body>
"""


def draw_page_bottom():
    return "\n</div><!-- End bodyFrame -->\n</body>\n</html>"


def draw_page_top(this_page):
    header = draw_page_header()
    admin_link = ""
    if current_session_g.get("role") == "Admin":
        admin_link = ("\t<a href='esltrack.php?page=adminLinks' "
                      + ("id='navCurrent' " if this_page == "adminLinks" else "")
                      + "title='esltrack Administration and Setup'>Admin</a> \n\n")
    else:
        admin_link = ("\t<a href='esltrack.php?page=tools' "
                      + ("id='navCurrent' " if this_page == "tools" else "")
                      + "title='esltrack system tools'>Tools</a> \n\n")

    return header + f"""
<div id="bodyFrame">

<div id="navBar">
 <a href='esltrack.php?page=home' title='Summary of your current project'
 {'id="navCurrent"' if this_page == 'home' else ''}>Home</a>

 <a href='esltrack.php?page=newIssue' title='Create a new defect report or Change Request'
 {'id="navCurrent"' if this_page == 'newIssue' else ''}>New Issue</a>

 <a href='esltrack.php?page=reports' title='Create simple and advanced reports'
 {'id="navCurrent"' if this_page == 'reports' else ''}>Reports</a>

 <a href='esltrack.php?page=projects' title='List of active projects'
 {'id="navCurrent"' if this_page == 'projects' else ''}>Projects</a>

{admin_link}
 <a href="javascript: this.print();" title="Printer friendly version of this page">Print</a>

 <a href="docs/help.html#home" title="Need help with this (or any) screen?" onclick='this.target="_blank";'>Help</a>

 <a href="esltrack.php?page=logout&amp;origin=user" title="Log off the esltrack System">Logout</a>
</div>

<hr class="legacyDivider" />

<noscript>
 <div class="cfgProb"> esltrack is pretty broken without Javascript. Please turn it back on. </div>
</noscript>

"""


def draw_login_page(project_table, failed="", session_expired="", no_cookies=""):
    project_cnt = len(project_table)
    options = ""
    for i in range(project_cnt):
        title = project_table[i]["Title"]
        selected = " selected='selected' " if CT.CT_DEFAULT_PROJECT == title else ""
        options += f"\t\t<option value='{title}'{selected}>{title}</option>\n"

    out = f"""<form id="loginForm" action="esltrack.php" method="post" onsubmit="return validateLogin(this);" >
<div id="login">
 <h1>Welcome to esltrack</h1>

 <div class="loginLabel"> Project </div>
 <div class="loginDD">
  <select name="userLogin[project_name]">
{options}
  </select>
 </div>

 <div class="loginLabel"> Username </div>
 <input type="text" name="userLogin[username]" value="" />

 <div class="loginLabel"> Password </div>
 <input type="password" name="userLogin[password]" value="" />

 <div class="loginButton"><input type="submit" value="Login" /></div>

 <input name="page" type="hidden" value="login" />

</div>
</form>

"""
    if failed:
        out += "<div class='loginMsg'><h1>Your login failed.</h1> Please enter a correct username and password.</div> \n"
    if session_expired:
        out += "<div class='loginMsg'><h2>Your session has expired.</h2> Please log in again.</div> \n"
    if no_cookies:
        out += ("<div class='loginMsg'><h1>Login unsuccessful.</h1> "
                "esltrack requires session cookies. Please ensure they are not being blocked.</div> \n")

    out += """
<script type="text/javascript" src="javascript/login_validate.js"> </script>

<noscript>
 <div class="cfgProb"> esltrack is pretty broken without Javascript. Please turn it on. </div>
</noscript>

<div class="cfgChkAccessCSS"> If you can see this, there are permission problems on the <em>style</em> directory. </div>
<div class="cfgChkAccessImg"> If you can see this, there are permission problems on the <em>images</em> directory. </div>
<div class="cfgChkAccessJs"> If you can see this, there are permission problems on the <em>javascript</em> directory. </div>

</body></html>
"""
    return out


def draw_prev_next_buttons(prev_id, next_id, bug_to_edit, destination_page, extra_buttons=False):
    id_ = str(bug_to_edit["ID"])

    if str(prev_id) == id_:
        prev_widget = "button"
    else:
        prev_widget = "submit"
    if str(next_id) == id_:
        next_widget = "button"
    else:
        next_widget = "submit"

    out = ""
    if not extra_buttons:
        out += "<div style='text-align: center;'>\n"

    out += f"""<div class='subNav'><form action='esltrack.php' method='get'><div class='subNavBar'>
   <input type='hidden' name='page' value='{destination_page}' />
   <input type='hidden' name='id'   value='{prev_id}' />
   <input type='{prev_widget}' value='&lt; Prev' class='buttonTiny' />
 </div></form>
"""
    if extra_buttons:
        out += f"""  <form action='esltrack.php' method='get'><div class='subNavBar'>
   <input type='hidden' name='page' value='editissue' />
   <input type='hidden' name='id'   value='{id_}' />
   <input type='submit' value='  Edit  ' class='buttonTiny' /></div></form>

  <form action='esltrack.php' method='get'><div class='subNavBar'>
   <input type='hidden' name='page' value='audit' />
   <input type='hidden' name='id'   value='{id_}' />
   <input type='submit' value='History' class='buttonTiny' /></div></form>
"""

    out += f""" <form action='esltrack.php' method='get'><div class='subNavBar'>
  <input type='hidden' name='page' value='{destination_page}' />
  <input type='hidden' name='id'   value='{next_id}' />
  <input type='{next_widget}' value='Next &gt;' class='buttonTiny' />
 </div></form></div>
"""
    if not extra_buttons:
        out += "</div>\n"
    return out


def draw_project_hotlist_widget(project_table, current_project, destination):
    options = ""
    for i in range(len(project_table)):
        title = project_table[i]["Title"]
        selected = " selected='selected'" if current_project == title else ""
        options += f"\t<option value=\"{title}\"{selected}>{title}</option>\n"

    tooltip = ' title="Change your current project"' if CT.CT_ENABLE_TOOLTIPS else ""

    return f"""
<form id="f" class="dropDown" action="esltrack.php" method="get">
<div>

<input type="hidden" name="page" value="changeproject" />
<input type="hidden" name="redir" value="{destination}" />

<select name="project"
 onchange='if (this.options[selectedIndex].value != "") document.forms[0].submit();' class="dropDown"
 {tooltip}>
{options}</select>

<input id='goButton' type='submit' value='Go' />
</div>
</form>

"""


def draw_read_only_table(data_table):
    out = "\n<table border='1' cellspacing='1' cellpadding='3' width='600' summary='Read-only data table'>\n"
    rowcnt = 0
    for column_name, value in data_table.items():
        out += "<tr class='" + ("rowEven" if rowcnt % 2 else "rowOdd") + "'>" \
               + "<td class='readOnlyLabel'>" + column_name.replace("_", " ") + "</td>"
        if column_name == "Attachment":
            value = f"<a href='attachments/{value}' onclick=\"this.target='_blank';\" >{value}</a>"
        out += "<td class='readOnlyData'>" + _nl2br(str(value)) + " &nbsp;</td></tr>\n"
        rowcnt += 1
    out += "</table>\n"
    return out


def _nl2br(s):
    return s.replace("\n", "<br />")


def bar_graph(values_array):
    if not values_array:
        raise RuntimeError("<br /><br /><pre>Fatal: No values passed to graph array! </pre>")

    out = ("<div class='barGraph'>\n\t<table cellpadding='0' cellspacing='0' border='0' "
           "width='100%' summary='Bar Graph'>\n")

    total = sum(values_array.values())

    for this_name, this_value in values_array.items():
        if total == 0:
            bar_value = " 0.0"
        else:
            bar_value = "%2.1f" % (this_value / total * 100)
            if bar_value == "100.0":
                bar_value = "100"

        out += f"<tr>\n\t<th>{this_name} </th>\n\t<td>"
        if float(bar_value) != 0:
            html_width = int(float(bar_value)) or 1
            out += (f"<img src='images/blue.gif' width='{html_width}' height='5' "
                    f"title='{this_value} ({bar_value}%) {this_name} items' alt='Bar Graph' />&nbsp;")
        out += f"</td>\n\t<td>{this_value}</td>\n\t<td>({bar_value}%)</td>\n</tr>\n"

    out += ("<tr class='graphTotal'>\n\t<th><strong>Total </strong></th>\n\t"
            "<td>&nbsp;</td>\n\t"
            f"<td><strong>{total}</strong></td>\n\t"
            "<td>(100%)</td>\n</tr>\n"
            "</table>\n</div>\n\n")
    return out


def draw_table(data_array, title, filter_, project_table=None):
    global current_session_g

    bug_statuses = [x.strip() for x in CT.CT_BUG_STATUSES.split(",")]
    bug_severities = [x.strip() for x in CT.CT_BUG_SEVERITIES.split(",")]

    status_counts = {s: 0 for s in bug_statuses}
    severity_counts = {s: 0 for s in bug_severities}

    if not data_array or not title:
        raise RuntimeError("Fatal: Draw table passed no data array or title!")

    out = '<script type="text/javascript" src="javascript/table_sort.js"> </script>'
    out += f"\n\n<div id='pageTitle'>{title}</div><br />\n"

    if project_table:
        out += draw_project_hotlist_widget(project_table, current_session_g.get("project", ""), "home")

    # Sort by Project, Severity (desc), ID
    data_array.sort(key=lambda x: (x.get("Project", "").lower(),
                                   x.get("Severity", "").lower(),
                                   x.get("ID", "")))

    if title in ("Active Projects", "Address Book"):
        element_entry = list(data_array[0].keys()) if data_array else []
    else:
        element_entry = [x.strip() for x in CT.CT_HOME_PAGE_DATA_COLUMNS.split(",")]

    if not element_entry:
        return ""

    out += "<div class='dataBlock'>\n"
    out += "<table id='results' cellspacing='1' border='0' width='100%' summary='Data Table'>\n"
    out += "<thead>\n\t<tr title='Click on column name to sort'>\n"

    hdr_cnt = 0
    for column_name in element_entry:
        if column_name == "Project" and "All Projects" not in title:
            continue
        if column_name == "Password":
            continue
        if column_name == "Assign_To":
            column_name = "Assigned_To"
        if column_name == "Developer_Response" and project_table:
            for tmp_proj in project_table:
                if tmp_proj.get("Title") == current_session_g.get("project"):
                    if "Preferred_Title" in tmp_proj:
                        column_name = tmp_proj["Preferred_Title"] + "_Response"
                    break
        out += f"\t\t<th onclick='SortTable({hdr_cnt});'> {column_name.replace('_', ' ')} </th>\n"
        hdr_cnt += 1
    out += "\t</tr>\n</thead>\n<tbody>"

    row_cnt = len(data_array)
    shown_row_cnt = 0

    for row in range(row_cnt):
        if not data_array[row].get("ID"):
            continue
        if "Delete_Bug" in data_array[row]:
            continue

        display = True
        if filter_:
            for filter_column, filter_value in filter_.items():
                if filter_column == "Project" and filter_value == "All":
                    continue
                if data_array[row].get(filter_column) != filter_value:
                    display = False
                    break

        if display:
            out += "\t<tr" + (" class='rowOdd'" if shown_row_cnt % 2 else " class='rowEven'") + ">\n"

            if "issue" in title.lower():
                for status in bug_statuses:
                    if data_array[row].get("Status") == status:
                        status_counts[status] += 1
                for severity in bug_severities:
                    if data_array[row].get("Severity") == severity:
                        severity_counts[severity] += 1

            for column in element_entry:
                if (column == "Project" and "All Projects" not in title) or column == "Password":
                    continue

                cell = str(data_array[row].get(column, "")).strip()

                if project_table and len(cell) > 40:
                    cell = cell[:40] + "..."

                if title != "Address Book":
                    if column == "ID" and cell:
                        accession = int(cell) if cell.isdigit() else 0
                        if title == "Active Projects":
                            cell = (f"<a href='esltrack.php?page=changeproject&amp;redir=home&amp;project="
                                    f"{urllib.parse.quote(data_array[row].get('Title', ''))}'>{cell}</a>")
                        else:
                            cell = f"<a href='esltrack.php?page=viewissue&amp;id={cell}'>{cell}</a>"
                        cell += f"<!-- {accession} -->"

                highlight_class = ""
                if (column == "Status"
                        and "Developer_Comment" not in data_array[row]
                        and "Developer_Response" not in data_array[row]):
                    highlight_class = " class='devResponse'"
                if (column == "Severity"
                        and data_array[row].get("Severity") in ("Fatal", "Critical")):
                    highlight_class = " class='devResponse'"

                if column in ("Last_Updated", "Submit_Time"):
                    human_date = parse_human_date(cell)
                    try:
                        unix_timestamp = int(datetime.strptime(cell, CT.CT_DATE_FORMAT).timestamp())
                    except Exception:
                        unix_timestamp = 0
                    cell = f"{human_date}<!-- {unix_timestamp} -->"

                out += f"\t\t<td{highlight_class}>"
                out += cell if cell else "&nbsp;"
                out += "</td>\n"

            out += "\t</tr>\n"
            shown_row_cnt += 1

    out += "</tbody></table>\n</div>\n"

    if "issue" in title.lower():
        out += f"<span id='resultsTotal'>Total issues: {shown_row_cnt} </span>"
        if shown_row_cnt > 0:
            out += ("\n<span id='resultsFooter'> (Oldest to newest, by severity. "
                    "Red status indicates response or comment needed.)</span><br /><br /><br /> \n")
            out += "<div class='graphTitle'> Count by Severity </div>\n"
            out += bar_graph(severity_counts)
            out += "<div class='graphTitle'> Count by Status </div>\n"
            out += bar_graph(status_counts)

    return out


def draw_add_edit_bug_form(project_table, user_table, bug_id=""):
    global current_session_g, xml_array_g

    project_cnt = len(project_table)
    user_cnt = len(user_table)

    user_role = None
    for i in range(user_cnt):
        if user_table[i].get("Full_Name") == current_session_g.get("user_full_name"):
            user_role = user_table[i].get("Role")
            break
    if not user_role:
        raise RuntimeError("<br /><center><h3>Fatal: No user role found!")

    if user_role == "Guest" and bug_id and not CT.CT_GUESTS_CAN_EDIT:
        raise RuntimeError("<br /><div class='cfgProb'>You are not authorized to edit issues</div>")
    if user_role == "Guest" and not bug_id and not CT.CT_GUESTS_CAN_CREATE:
        raise RuntimeError("<br /><div class='cfgProb'>You are not authorized to create new issues</div>")

    browser_name = [x.strip() for x in CT.CT_MAJOR_BROWSERS.split(",")]
    bug_severity = [x.strip() for x in CT.CT_BUG_SEVERITIES.split(",")]
    bug_status = [x.strip() for x in CT.CT_BUG_STATUSES.split(",")]
    OS_name = [x.strip() for x in CT.CT_MAJOR_OS.split(",")]
    developer_response = [x.strip() for x in CT.CT_DEVELOPER_RESPONSES.split(",")]

    bug_to_edit = None
    this_project = None
    prev_id = next_id = None

    if bug_id:
        parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
        bug_array = xml_array_g
        index = get_array_index_for_id(bug_array, str(bug_id))
        bug_to_edit = bug_array[index]
        prev_id = get_prev_bug_id(bug_array, index)
        next_id = get_next_bug_id(bug_array, index)

    out = ""
    if bug_to_edit:
        out += (f"<div id='pageTitle'> Edit  {current_session_g.get('project', '')} Issue "
                f"<a href='esltrack.php?page=viewissue&amp;id={bug_id}'>{bug_id}</a></div>\n")
        out += draw_prev_next_buttons(str(prev_id), str(next_id), bug_to_edit, "editissue")
    else:
        out += f"\n\n<div id='pageTitle'> Report a New Issue for {current_session_g.get('project', '')} </div>\n"

    out += f"""
 <form enctype="multipart/form-data" id="bF" action="esltrack.php" method="post"
  onsubmit="return checkMissing(this);" >
 <div id="bugForm">

 <input name="MAX_FILE_SIZE" type="hidden" value="{CT.CT_MAX_UPLOAD_FILE_SIZE}" />
 <input name="page" type="hidden" value="saveissue" />
"""

    if debug_g:
        out += '<input name="debug" type="hidden" value="1" />'

    out += """
 <div class="innerBorderFix">
 <table cellpadding="5" cellspacing="4" border="1" width="100%" summary="Issue Report">
"""

    if bug_to_edit:
        if "Updated_By" in bug_to_edit:
            msg = "Last updated "
        else:
            bug_to_edit["Updated_By"] = bug_to_edit.get("Submitted_By", "")
            msg = "Submitted "
        msg += bug_to_edit.get("Last_Updated", "") + " by " + bug_to_edit.get("Updated_By", "")
        out += (f"\n\t<tr><td colspan='3'> {msg} "
                f"<a href='esltrack.php?page=audit&amp;id={bug_to_edit.get('ID', '')}'>[History]</a></td></tr>\n")

    out += "\t<tr><td id='addEditBugColumn' colspan='2'>\n\n\tProject\n\t<select name='bug_data[Project]'>"

    p = bug_to_edit.get("Project") if bug_to_edit else current_session_g.get("project", "")

    for i in range(project_cnt):
        title = project_table[i]["Title"]
        out += f"\n\t\t<option value=\"{title}\""
        if title == p:
            out += " selected='selected' "
            this_project = project_table[i]
        out += f">{title}</option>"
    out += "\n\t</select></td>\n\n"

    # Module / Version
    module_val = _esc_quote(bug_to_edit.get("Module", "")) if bug_to_edit else ""
    version_val = _esc_quote(bug_to_edit.get("Version", "")) if bug_to_edit else ""
    out += f"""
 <td>Module or Screen Name
 <input name="bug_data[Module]" type="text" size="25" maxlength="25" value='{module_val}' /></td>
 <td>Version
 <input name="bug_data[Version]" type="text" size="10" maxlength="12" value='{version_val}' /></td>
 </tr>
 <tr>

 <td>Severity*
 <select name='bug_data[Severity]'>
\t\t<option value=''></option>"""

    for severity in bug_severity:
        if ((not bug_to_edit and severity == CT.CT_DEFAULT_SEVERITY)
                or (bug_to_edit and bug_to_edit.get("Severity") == severity)):
            out += f"\n\t\t<option value='{severity}' selected='selected'>{severity}</option>"
        else:
            out += f"\n\t\t<option value='{severity}'>{severity}</option>"

    priority_val = _esc_quote(bug_to_edit.get("Priority", "")) if bug_to_edit else "0"
    crco_val = _esc_quote(bug_to_edit.get("CRCO", "")) if bug_to_edit else ""
    summary_val = _esc_quote(bug_to_edit.get("Summary", "")) if bug_to_edit else ""

    out += f"""
 </select>
 </td>
 <td>Priority
 <input name="bug_data[Priority]" type="text" size="2" maxlen="2" value='{priority_val}' />
  </td>
 <td>CRCO
 <input name="bug_data[CRCO]" type="text" size="7" maxlen="7" value='{crco_val}' />
  </td>


 <td colspan="2"><em>Brief</em> Summary of Problem*
 <input name="bug_data[Summary]" type="text" size="40" maxlength="55" value='{summary_val}' /></td>

  </tr>
  <tr>
   <td colspan="4">Full Description*<em> (the more details the better!)</em>
   <textarea name="bug_data[Description]" rows="7" cols="50" {CT.CT_OPTIONAL_UGLY_NN4_HACK} >"""

    if bug_to_edit:
        out += bug_to_edit.get("Description", "")
    out += "</textarea></td>\n  </tr>\n  <tr>\n"

    if bug_to_edit:
        out += "   <td colspan='3'>Attachment: "
    else:
        out += "   <td colspan='4'>Attachment <em>(screen print, data file, etc.)</em>"

    if bug_to_edit and "Attachment" in bug_to_edit:
        att = bug_to_edit["Attachment"]
        out += (f"&nbsp;<span class='txtSpecial'>"
                f"<a href='attachments/{att}' onclick=\"this.target='_blank';\" >{att}</a></span>"
                f"<input name='bug_data[Attachment]' type='hidden' value='{att}' />")
    else:
        out += "<input name='Attachment' type='file' size='40' />\n"

    out += "\n   </td>\n"

    if bug_to_edit:
        if (CT.CT_QA_ENFORCE_PRIVS
                and current_session_g.get("role") not in [x.strip() for x in CT.CT_QA_WHO_CAN_CLOSE.split(",")]):
            out += "\n<td><span class='txtSpecial'> [ ] </span> Delete Report </td>"
        else:
            out += ("\n<td><span class='bugFormGroup'>"
                    "\n<input type='checkbox' name='bug_data[Delete_Bug]' value='Y' "
                    "onclick='if (this.checked) "
                    "return confirm(\"Checking the Delete box will permanently erase this report."
                    "\\nIf you really want to delete this report, click OK then press Save. "
                    "\\nTo simply close the issue, cancel now and change the Status category."
                    "\");' />&nbsp; Delete Report </span></td>")

    out += "\n  </tr>\n"

    if this_project and this_project.get("Project_Type") == "Web-Based":
        out += """
  <tr>
  <td colspan='2'>
   Tested Browser  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  Browser-Specific? <br />
   <span class='bugFormGroup'>
   <select name='bug_data[Tested_Browser]'>"""
        for browser in browser_name:
            if ((bug_to_edit and bug_to_edit.get("Tested_Browser") == browser)
                    or (not bug_to_edit and browser == CT.CT_DEFAULT_BROWSER)):
                out += f"\t\t\t\t<option value='{browser}' selected='selected'>{browser}</option>\n"
            else:
                out += f"\t\t\t\t<option value='{browser}'>{browser}</option>\n"
        out += "   </select>\n\n   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n\n"

        bs = bug_to_edit.get("Browser_Specific", "") if bug_to_edit else ""
        out += f"   <input type='radio' name='bug_data[Browser_Specific]' value='Y' {'checked' if bs == 'Y' else ''} />Y &nbsp;\n"
        out += f"   <input type='radio' name='bug_data[Browser_Specific]' value='N' {'checked' if bs == 'N' else ''} />N &nbsp;\n"
        dk_checked = "checked" if (bs == "" or not bug_to_edit) else ""
        out += f"   <input type='radio' name='bug_data[Browser_Specific]' value='' {dk_checked} />D/K\n"
        out += "   </span>\n  </td>\n  <td>\n   Tested OS\n   <select name='bug_data[Tested_OS]'>"
        for OS in OS_name:
            if ((not bug_to_edit and OS == CT.CT_DEFAULT_OS)
                    or (bug_to_edit and bug_to_edit.get("Tested_OS") == OS)):
                out += f"<option value='{OS}' selected='selected'>{OS}</option> \n"
            else:
                out += f"<option value='{OS}'>{OS}</option> \n"
        out += "\n  </select>\n  </td>\n </tr>\n"

    submitter = bug_to_edit.get("Submitted_By") if bug_to_edit else current_session_g.get("user_full_name", "")
    out += f"""
 <tr>
  <td >
  Submitted By <div class='txtSpecial'> &nbsp;&nbsp;{submitter} &nbsp;</div>
  <input name='bug_data[Submitted_By]' type='hidden' value='{submitter}' />
  </td>
  <td colspan="2"><div id="addEditActionButtons" class="bugFormGroup">
    <input type='submit' value=' Save ' />
    <input type='button' value='Cancel' onclick="location.replace('esltrack.php?page=home');" />
    <input type='reset' value=' Undo ' />
  </div></td>

  <td>
"""

    if bug_to_edit:
        if (CT.CT_QA_ENFORCE_PRIVS
                and current_session_g.get("role") not in [x.strip() for x in CT.CT_QA_WHO_CAN_CLOSE.split(",")]):
            status = bug_to_edit.get("Status", "")
            out += (f"Status: <div class='txtSpecial'>&nbsp; {status}</div>"
                    f"<input name='bug_data[Status]' type='hidden' value='{status}' />\n")
        else:
            out += "Status\n<select name='bug_data[Status]'>\n"
            for status in bug_status:
                out += f"\t<option value='{status}' "
                if bug_to_edit.get("Status") == status:
                    out += " selected='selected' "
                out += f">{status}</option>\n"
            out += "\n</select>\n"
    else:
        out += ("Status: <div class='txtSpecial'>&nbsp; Open</div>"
                "<input name='bug_data[Status]' type='hidden' value='Open' />\n")

    tracker_val = _esc_quote(bug_to_edit.get("Tracker_Number", "")) if bug_to_edit else ""
    out += f"""
  </td>
 <td>
Tracker Number
 <input name="bug_data[Tracker_Number]" type="text" size="7" maxlen="15" value='{tracker_val}' />
 </tr>
"""

    if bug_to_edit:
        preferred = this_project.get("Preferred_Title", "Developer") if this_project else "Developer"
        comment = bug_to_edit.get("Developer_Comment", "").replace("__", "\r\n")
        corrected_val = _esc_quote(bug_to_edit.get("Corrected_Version", ""))
        out += f"""
 <tr>
  <td colspan="3">


  {preferred} Comment 
   <textarea name="bug_data[Developer_Comment]" rows="4" cols="50" {CT.CT_OPTIONAL_UGLY_NN4_HACK} >{comment}</textarea>
  </td>
  <td>

  {preferred} Response 

   <select name='bug_data[Developer_Response]'>"""
        for response in developer_response:
            if ((not bug_to_edit and response == CT.CT_DEFAULT_DEVELOPER_RESPONSE)
                    or (bug_to_edit.get("Developer_Response") == response)):
                out += f"<option value=\"{response}\" selected='selected'>{response}</option> \n"
            else:
                out += f"<option value=\"{response}\">{response}</option> \n"
        out += f"""</select>
  </td>
  <td>
    Corrected Version
 <input name="bug_data[Corrected_Version]" type="text" size="7" maxlen="7" value='{corrected_val}' />

  
   </td>
 </tr>
"""

    out += "<tr>\n\t\t<td colspan='3'>\n\t\tAssign To: \n<div class='bugFormGroup'>"
    out += "<select name='bug_data[Assign_To]'><option value=''>&nbsp;</option>"

    assignee = bug_to_edit.get("Assign_To") if bug_to_edit else None
    for i in range(user_cnt):
        full_name = user_table[i]["Full_Name"]
        sel = " selected='selected' " if full_name == assignee else ""
        out += f"<option value='{full_name}'{sel}>{full_name}</option>\n"
    out += "</select>"

    if CT.CT_ENABLE_EMAIL:
        out += ("<label title='Send an email to the Assignee'><input type='checkbox' name='send_mail' value='1' />"
                "<img src='images/email.gif' \n\t\t onclick='send_mail.checked=!send_mail.checked' alt='email' /></label> \n")
        out += ("<label title='Send an email to these people too'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
                "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cc: ")
        out += "<select name='cc_list[]' multiple='multiple' size='3'>"
        for i in range(user_cnt):
            out += (f"<option value=\"{user_table[i]['Email']}\">"
                    f"{user_table[i]['Full_Name']}</option>\n")
        out += "</select></label>\n"

    out += "\n  </div></td>\n </tr>\n </table>\n </div>\n"

    if bug_to_edit:
        out += f"<input name='id' type='hidden' value='{bug_to_edit.get('ID', '')}' />\n"
        out += f"<input name='original_submit_time' type='hidden' value='{bug_to_edit.get('Submit_Time', '')}' />\n"

    out += """
 </div>
 </form>
 <script type="text/javascript" src="javascript/bugform_prevalidate.js"> </script>
 <script type="text/javascript" src="javascript/form_validate.js"> </script>

"""
    return out


def _esc_quote(s):
    return str(s).replace("'", "&#039")


def draw_admin_page():
    return f"""
 <div id='pageTitle'> Administrator Tools </div><br />
 <div style='margin-left: 20%; margin-top: 0px; margin-bottom: 0px;'>
 <hr width='80%' class="hrBlack" align="left">
  Maintenance
  <ul>
   <li><a href="esltrack.php?page=addproject">Add a Project</a></li>
   <li><a href="esltrack.php?page=adduser">Add a User</a></li>
   <li><a href="esltrack.php?page=changepassword">Change a Password</a></li>
   <li><a href="esltrack.php?page=export">Export Data</a></li>
   <li><a href="esltrack.php?page=backup">Backup XML Databases</a></li>
   <li><a href="esltrack.php?page=users">List Active Users</a></li>
   <li><a href="esltrack.php?page=deletedissues">List Deleted Issues</a></li>
  </ul>
  Technical References
  <ul>
   <li><a href="docs/INSTALL.txt" target="_blank">Installation Guide</a></li>
   <li><a href="docs/CUSTOMIZING.txt" target="_blank">Customizing esltrack</a></li>
   <li><a href="docs/TROUBLESHOOTING.txt" target="_blank">Troubleshooting Guide</a></li>
   <li><a href="docs/CHANGELOG.txt" target="_blank">esltrack {CT.CT_CODE_VERSION} Changelog</a></li>
   <li><a href="docs/FUNCTIONS.txt" target="_blank">Functions Reference</a></li>
   <li><a href="docs/FILES.txt" target="_blank">Inventory of all esltrack Files</a></li>
  </ul>
  License
  <ul>
   <li><a href="docs/LICENSE.txt" target="_blank">CodeTrack is Free software</a></li>
   <li><a href="docs/GPL_FAQ.html" target="_blank">FAQ about the GNU Public License (GPL)</a></li>
  </ul>
  <br />
  <b>Support questions? <a href="mailto:codetrack@openbugs.org">Write us</a> we're happy to help!</b>
	<br /><br /><i>Copyright &copy; 2001-2006 Kenneth White</i>
 </div></div>
"""


def draw_export_options(current_project):
    return f"""
 <div id='pageTitle'> Data Export Wizard </div><br />
 <form action='esltrack.php' method=POST>
 <div style='font-family: verdana, sans-serif; font-size: 10pt;'>
 <div style='margin-left: 15%; margin-top: 0px; margin-bottom: 0px;'>
 <input type=hidden name=page value=export>
 File Format: <br />
 <em>
  &nbsp;&nbsp;<input type='radio' name='export_options[file_format]' value='csv' checked='checked'> Excel file (CSV)<br />
  &nbsp;&nbsp;<input type='radio' name='export_options[file_format]' value='prn'> Tab-separated text file <br />
  &nbsp;&nbsp;<input type='radio' name='export_options[file_format]' value='sql'> SQL Import file <br />
  &nbsp;&nbsp;<input type='radio' name='export_options[file_format]' value='xml_download'> XML file <br />
  &nbsp;&nbsp;<input type='radio' name='export_options[file_format]' value='xml'> XML tree browser <br />
 </em><br />
 History: <br />
 <em>
  &nbsp;&nbsp;<input type='radio' name='export_options[show_history]' value=1> Show all changes for each issue <br />
  &nbsp;&nbsp;<input type='radio' name='export_options[show_history]' value=0 checked='checked'> Only show the current status of each issue <br />
 </em><br />
 Sort issues by: <br />
 <em>&nbsp;&nbsp;<input type='radio' name='export_options[sort_type]' value=descending checked='checked'>
   Oldest first (Ascending by ID)<br />
  &nbsp;&nbsp;<input type='radio' name='export_options[sort_type]' value=ascending> Newest first <br />
 </em><br />
 <input type=checkbox name='export_options[project_filter]' value='{current_project}' checked='checked'>
 Restrict export to <strong>{current_project}</strong> issues <br />
 <input type=checkbox name='export_options[show_deleted]' value='y'> Include deleted issues in export <br /><br />
 <input type=submit value="Download">
 <br /><br /><em>Note: History and Sort options do not apply to XML exports</em>
 </div>
 </div>
 </form>
 """


def draw_reports_page(project_table, user_table):
    current_project = current_session_g.get("project", "")
    user_full_name = current_session_g.get("user_full_name", "")

    link1 = ("esltrack.php?page=filter&amp;filter[Status]=Open"
             f"&amp;filter[Assign_To]={urllib.parse.quote(user_full_name)}")
    link2 = ("esltrack.php?page=filter"
             f"&amp;filter[Project]={urllib.parse.quote(current_project)}"
             "&amp;filter[Status]=Open"
             f"&amp;filter[Assign_To]={urllib.parse.quote(user_full_name)}")
    link3 = ("esltrack.php?page=filter"
             f"&amp;filter[Project]={urllib.parse.quote(current_project)}"
             f"&amp;filter[Submitted_By]={urllib.parse.quote(user_full_name)}")

    project_options = ""
    for i in range(len(project_table)):
        title = project_table[i]["Title"]
        project_options += f'\t\t\t<option value="{title}">{title}</option>\n'

    severity_options = ""
    for severity in [x.strip() for x in CT.CT_BUG_SEVERITIES.split(",")]:
        severity_options += f'\t\t\t<option value="{severity}">{severity}</option>\n'

    status_options = ""
    for status in [x.strip() for x in CT.CT_BUG_STATUSES.split(",")]:
        status_options += f'\t\t\t<option value="{status}">{status}</option>\n'

    submitted_options = ""
    for i in range(len(user_table)):
        fn = user_table[i]["Full_Name"]
        submitted_options += f'\t\t\t<option value="{fn}">{fn}</option>\n'

    assign_options = ""
    for i in range(len(user_table)):
        fn = user_table[i]["Full_Name"]
        assign_options += f'\t\t\t<option value="{fn}">{fn}</option>\n'

    return f"""
 <div id='pageTitle'> Search and Reports Wizard </div>
 <div style='margin-left: 10%;'>
 <hr width='80%' class="hrBlack" align="left">

  Quick Reports
  <ul>
   <li><a href='{link1}' >All open issues assigned to me</a></li>
   <li><a href='{link2}' >Open <strong>{current_project}</strong> issues assigned to me</a></li>
   <li><a href='{link3}' ><strong>{current_project}</strong> issues that I created</a></li>
   <li><a href='esltrack.php?page=summary'>Quality Assurance Statistics for <strong>{current_project}</strong></a></li>
  </ul>

 <hr width='80%' class="hrBlack" align="left">

 Custom Reports <br /><br />

 <div style='margin-left: 3%; margin-top: 0px; margin-bottom: 0px;'>
 <form action="esltrack.php" method="get">
 <table border=0 summary="">
  <tr class='txtSmall'>
   <td>Project</td>
   <td>Severity</td>
   <td>Status</td>
   <td>Submitted By</td>
   <td>Assigned To</td>
   <td> &nbsp; </td>
  </tr>
  <tr>
   <td>
    <select name='filter[Project]'>
     <option value=''>(All)</option>
{project_options}\t\t</select>
\t\t</td>
\t\t<td>

   <select name='filter[Severity]'>
\t\t<option value=''>(All)</option>
{severity_options}\t</select>

\t\t</td>
\t\t<td>

   <select name='filter[Status]'>
\t\t<option value=''>(All)</option>
{status_options}\t</select></td>


\t\t<td>
\t\t<select name='filter[Submitted_By]'>
\t\t<option value=''>(Anyone)</option>
{submitted_options}    </select>
   </td>
   <td>
    <select name='filter[Assign_To]'>
     <option value=''>(Anyone)</option>
{assign_options}    </select>
   </td>
   <td>&nbsp;<input type=submit value='Go!'> </td>
  </tr>
 </table>
 <input type=hidden name='page' value='filter'>
 </form>
 </div>

 <hr width='80%' class="hrBlack" align="left">

 Full Text Search <br /><br />

 <div style='margin-left: 3%; margin-top: 0px; margin-bottom: 0px;'>
 <form action="esltrack.php" method="get">
  <input type=hidden name='page' value='searchissue'>
  <input type=text size='50' name=pattern>&nbsp;
  <input type=submit value='Go!'> <br /><br />
 Look for:
		<input type='radio' name='search_options[phrase]'	  value=0 CHECKED>Any word &nbsp;&nbsp;&nbsp;
		<input type='radio' name='search_options[phrase]'	  value=1>Entire Phrase <br />
	Within: &nbsp;&nbsp;
		<input type=checkbox name='search_options[summary]'	value=1 CHECKED>Summaries &nbsp;
		<input type=checkbox name='search_options[description]' value=1 CHECKED>Descriptions &nbsp;
		<input type=checkbox name='search_options[comment]'	value=1 CHECKED>Comments <br />
	</form>
	</div>

	</div>
	"""


def draw_change_password_form(user_table):
    role = current_session_g.get("role", "")
    admin = False
    widget_label = ""
    widget = ""

    if role == "Admin":
        admin = True
        widget_label = "&nbsp; User to update <br />&nbsp;"
        widget = "<select name='user_data[ID]'> \n"
        for user in user_table:
            widget += f"\t\t <option value='{user['ID']}'>{user['Full_Name']}</option> \n"
        widget += "\t\t </select><br />&nbsp;\n"
    else:
        admin = False
        widget_label = "Old Password<br />&nbsp; &nbsp;"
        id_ = ""
        for user in user_table:
            if user.get("Username") == current_session_g.get("username"):
                id_ = user["ID"]
                break
        widget = (f"<input type=hidden name='user_data[ID]' value='{id_}' >\n"
                  "<input type=password name='old_pw' maxlength=25><br />&nbsp; &nbsp;")

    out = ('<script type="text/javascript" src="javascript/form_validate.js"> </script>' + "\n"
           '<center><div class="txtSmall"><br /><b>  Password Update  </b><br /><br /><br /></div>' + "\n"
           '<form id="passwordForm" action="esltrack.php" method=POST '
           f"onsubmit='return checkPasswords(this, {CT.CT_MIN_PASSWORD_LENGTH});' >")

    if debug_g:
        out += "\t<input type=hidden name='debug' value='1'>\n"

    out += f"""
	<table border=0 cellpadding=5 cellspacing=0 bgcolor="#c0c0c0" summary="Password Update">
		<tr>
			<td class='txtSmall' align=RIGHT>{widget_label}</td>
			<td>{widget}</td>
		</tr>
		<tr>
			<td class='txtSmall' align=RIGHT>New Password</td>
			<td><input type=password name="user_data[Password]" maxlength=25></td>
		</tr>
		<tr>
			<td class='txtSmall' align=RIGHT>Confirmation</td>
			<td><input type=password name="retyped_pw" maxlength=25></td>
		</tr>
		<tr>
			<td align=LEFT><input type=submit value='Save'></td>
			<td align=RIGHT><input type=button value='Cancel' onclick='location.href="esltrack.php?page=home";'></td>
		</tr>
	</table>
	<input type=hidden name='page' value='savepassword'>
	</form>
	</center>
"""
    if admin:
        out += "\t<script type='text/javascript'> document.forms['passwordForm'].elements['user_data[Password]'].focus(); </script>\n"
    else:
        out += "\t<script type='text/javascript'> document.forms['passwordForm'].elements['old_pw'].focus(); </script>\n"
    return out


def draw_user_maintenance_form():
    roles = [x.strip() for x in CT.CT_ACCESS_LEVELS.split(",")]
    role_options = ""
    for role in roles:
        if role == CT.CT_DEFAULT_ACCESS_LEVEL:
            role_options += f"<option value=\"{role}\" selected='selected' > {role} </option>\n"
        else:
            role_options += f"<option value=\"{role}\">{role}</option>\n"

    pedantic = ""
    if CT.CT_PEDANTIC_VALIDATION:
        pedantic = " &amp;&amp; validEmail(this['user_data[Email]'].value) "

    debug_input = "<input type=hidden name=debug value='y'>" if debug_g else ""

    return f"""
	<center>
	<div class='txtSmall'><br /><b>Add a New User</b><br /><br /><br /></div>

	<form id="userForm" action="esltrack.php" method="POST"
		onSubmit="return checkMissing(this) {pedantic};" >

	<table border=2 cellpadding=7 cellspacing=0 bgcolor="#E4E2DF" summary="User Maintenance">
		<tr>
			<td class='txtSmall'>
				<input name="page" type="hidden" value="saveuser">
				<input name="user_data[ID]" type="hidden" value="-1">
				First Name* <br /><input name="user_data[First_Name]" type="text" size="20" maxlength="25">&nbsp;&nbsp;
			</td>
			<td class='txtSmall'>
				Last Name*  <br /><input name="user_data[Last_Name]" type="text" size="20" maxlength="25">
				<input name="user_data[Full_Name]" type="hidden" value="x">
				<input name="user_data[Initials]"  type="hidden" value="x">
				<input name="user_data[Username]"  type="hidden" value="x">
			</td>
		</tr>
		<tr>
			<td class='txtSmall' colspan=2>e-mail address*<br />
				<input name="user_data[Email]" type="text" size="30" maxlength="60">
			</td>
		</tr>
		<tr>
			<td class='txtSmall' colspan=2>Phone Number<br />
				<input name="user_data[Phone]" type="text" size="20" maxlength="32">
			</td>
		</tr>
		<tr>
			<td class='txtSmall' colspan=2>Role<br />
				<select name='user_data[Role]'>
{role_options}				</select>
			</td>
		</tr>
		<tr>
			<td class='txtSmall' colspan=2>Username <em>(leave blank to autogenerate)</em><br />
				<input name="user_data[Username]" type="text" size="20" maxlength="32">
			</td>
		</tr>
		<tr>
			<td class='txtSmall' colspan=2>Initial Password <em>(leave blank to autogenerate)</em><br />
				<input name="user_data[Password]" type="text" size="20" maxlength="32">
			</td>
		</tr>
		<tr>
			<td class='txtSmall' colspan=2><input type=submit value='Save'>

			&nbsp;&nbsp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <input type=checkbox value=1 name="notify_user"> e-mail information to user

			{debug_input}
			</td>
		</tr>
	</table>
	</form>
	</center>
	<script type="text/javascript" src="javascript/userform_prevalidate.js"> </script>
	<script type="text/javascript" src="javascript/form_validate.js"> </script>
"""


def draw_project_maintenance_form(user_table):
    user_options = ""
    for i in range(len(user_table)):
        fn = user_table[i]["Full_Name"]
        first = user_table[i]["First_Name"]
        sel = " selected='selected'>" if fn == CT.CT_DEFAULT_PROJECT_LEAD else ">"
        user_options += f'\t\t\t<option value="{first}"{sel}{fn} </option>\n'

    debug_input = "<input type=hidden name=debug value='y'>" if debug_g else ""

    return f"""
	<center>
	<div class='txtSmall'><br /><b>Add a New Project</b><br /><br /><br /></div>

	<form id="projectForm" action="esltrack.php" method="POST" onSubmit="return checkMissing(this);" >

	<table border=2 cellpadding=7 cellspacing=0 bgcolor="#E4E2DF" summary="Project Maintenance">
		<tr>
			<td class='txtSmall'>Project Title <em>(One or Two Words, or an acronym) </em><br />
				<input name="page" type="hidden" value="saveproject">
				<input name="project_data[ID]" type="hidden" value="-1">
				<input name="project_data[Title]" type="text" size="20" maxlength="25" value="">
			</td>
		</tr>
		<tr>
			<td class='txtSmall'>Lead Developer or Analyst<br />
				<select name='project_data[Test_Lead]'>
{user_options}\t\t</select>

			</td>
		</tr>
		<tr>
			<td class='txtSmall'>Project Description <br />
				<input name="project_data[Description]" type="text" size="55" maxlength="65" value="">
			</td>
		</tr>
		<tr>
			<td class='txtSmall'>
				Type of Project <br />
				<input name="project_data[Project_Type]" type=radio value="Web-Based" CHECKED>Web-Based &nbsp;&nbsp;&nbsp;&nbsp;
				<input name="project_data[Project_Type]" type=radio value="Desktop Application">Desktop Application &nbsp;&nbsp;&nbsp;&nbsp;
				<input name="project_data[Project_Type]" type=radio value="Data Analysis">Data Analysis
			</td>
		</tr>
		<tr>
			<td class='txtSmall'>
				Preferred Title of Responding Team Members <br />
				<input name="project_data[Preferred_Title]" type=radio value="Developer" CHECKED>Developer &nbsp;&nbsp;&nbsp;&nbsp;
				<input name="project_data[Preferred_Title]" type=radio value="Analyst">Analyst &nbsp;&nbsp;&nbsp;&nbsp;
				<input name="project_data[Preferred_Title]" type=radio value="Engineer">Engineer
			</td>
		</tr>
		<tr>
			<td class='txtSmall'><input type=submit value='Save'>
			{debug_input}
			</td>
		</tr>
	</table>
	</form>
	</center>
	<script type="text/javascript" src="javascript/form_validate.js"> </script>
	<script type="text/javascript" src="javascript/projectform_prevalidate.js"> </script>
"""


def draw_view_bug_page(bug_id):
    global xml_array_g
    parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
    bug_array = xml_array_g
    index = get_array_index_for_id(bug_array, str(bug_id))
    bug_to_edit = bug_array[index]

    out = f"\n\t<div id='pageTitle'> Issue # {bug_id} </div><br />\n"
    prev_id = get_prev_bug_id(bug_array, index)
    next_id = get_next_bug_id(bug_array, index)
    out += draw_prev_next_buttons(str(prev_id), str(next_id), bug_to_edit, "viewissue", "all_buttons")
    out += "\n"
    out += draw_read_only_table(bug_to_edit)
    return out


def draw_xml_backups_page(success=""):
    out = ("<div class='txtSmall'><center><br /><b>XML Database Backup Utility</b></center><br />"
           "<div style='margin-left: 20%; margin-top: 0px; margin-bottom: 0px;'>"
           "<hr width='80%' class='hrBlack' align='left'><br />"
           f"Existing Entries in <em>{CT.CT_XML_BACKUPS_DIRECTORY}</em> :\n<br />")

    try:
        file_list = [f for f in os.listdir(CT.CT_XML_BACKUPS_DIRECTORY)
                     if f not in (".", "..", "index.html")]
    except OSError:
        raise RuntimeError(
            f'<br /><br /><H3>Fatal:  Could not read the "{CT.CT_XML_BACKUPS_DIRECTORY}" directory! '
            "Make sure it is read+writable by the web server.")

    if file_list:
        file_list.sort()
        out += "\n<form action='#'>\n<textarea ROWS='8' COLS='35'>\n"
        for entry in file_list:
            out += f"{entry}\n"
        out += "</textarea>\n</form>\n\n"
    else:
        out += "<br /><em>(none)</em><br />\n"

    out += ("<br /><br /><form action='esltrack.php' method=post>\n"
            "<input type=submit value='Backup Now'>\n"
            "<input type=hidden name='page' value='dobackup'></form>\n")

    if success:
        out += "<br /><em>Backups successfully created.</em>\n"

    out += "\n</div></div>\n"
    return out


def draw_tools_page():
    address_link = ""
    if CT.CT_DISPLAY_ADDRESSES:
        address_link = "\t\t<li><a href='esltrack.php?page=users'>Address Book</a></li>\n"

    return f"""
	<div id='pageTitle'> System Tools </div><br />
	<div style='margin-left: 10%;'>
	<hr width='80%' class="hrBlack" align="left">
		<ul>
{address_link}			<li><a href="esltrack.php?page=changepassword">Change Your Password</a></li>
			<li><a href="esltrack.php?page=export">Export Data</a></li>
		</ul>
		<br />
		<br />
		<b>A Few Tips</b><br />
		<ol>
			<li>To create a quick defect report in Excel, just click Export Data (above), then click Download.</li><br /><br />
			<li>On the New and Edit screens, send an email notification to other team members by clicking the envelope.</li><br /><br />
			<li>To see an audit trail of all the changes that have been made to an issue, click on the ID, then History.</li><br /><br />
			<li>Print pages in landscape mode.</li><br /><br />
			<li>If printing from Internet Explorer, enable prettier reports: Tools/Internet Options/Advanced/Print Background colors.</li><br /><br />
			<li>The Help button above is context-sensitive -- the text changes depending on which screen you're on.</li>
		</ol>
  <br /><br /><br />esltrack is <a href="http://www.gnu.org/licenses/gpl.html#SEC1" target=_blank>Free</a> software. Copyright &copy; 2001 Kenneth White
"""


def draw_project_access_form(project_table, user_table, permission_table):
    current_project = current_session_g.get("project", "")

    out = '<script type="text/javascript" src="javascript/form_validate.js"> </script>'
    out += f"<div id='pageTitle'> Select Authorized Users for: &nbsp;{current_project} </div><br />\n"
    out += draw_project_hotlist_widget(project_table, current_project, "projectaccess")

    this_project_id = ""
    for i in range(len(project_table)):
        if project_table[i].get("Title") == current_project:
            this_project_id = project_table[i].get("ID", "")
            break

    out += f"""
<form id="maintenance" action="esltrack.php" method="POST" style="margin-top: 0px; margin-left:0; margin-bottom: 0px;">
 <input type="hidden" name="page" value="savepermissions">
 <input type="hidden" name="permission_data[Project_ID]" value="{this_project_id}" >
 <table cellspacing=0 cellpadding=0 border=1 style="margin-top: 0px; margin-left:0; margin-bottom: 5px;" summary="Permissions">
 <tr class='rowHeader'>
  <td style='text-align: center;' width='10'><input type=checkbox name=toggleBoxes onClick='toggle_checkboxes(document.forms["maintenance"]);'></td>
  <td style='text-align: left;'><strong> Authorized User &nbsp;&nbsp;</strong></td>
  <td style='text-align: left;'><strong> Role &nbsp;&nbsp;</strong></td>
 </tr>
"""

    row = 0
    for user in user_table:
        checked = ""
        if user.get("Role") == "Admin":
            widget_type = "hidden"
            extra = "[<b>x</b>]"
        else:
            widget_type = "checkbox"
            extra = ""
            if authorized_user(this_project_id, user.get("ID"), permission_table):
                checked = ' checked="checked" '

        row_color = "rowEven2" if row % 2 else "rowOdd"
        row += 1

        out += (f"\n<tr>\n\t<td class='{row_color}' align=center height=22>"
                f"<input type={widget_type} name='permission_data[User_ID][{row}]' "
                f"value='{user.get('ID')}' {checked} >{extra}</td>\n"
                f"\t<td class='{row_color}' > &nbsp; "
                f"{user.get('Last_Name')}, {user.get('First_Name')}&nbsp;&nbsp;</td>\n"
                f"\t<td class='{row_color}' >&nbsp; {user.get('Role')} &nbsp;</td>\n</tr>")

    out += "\n</table>\n<input type='submit' value='Save'>\n</form>\n\n"
    return out


# ===========================================================================
#  Business logic functions
# ===========================================================================

def create_summary(project_table):
    global xml_array_g
    out = ("\n<br /><br /><div id='pageTitle'>Quality Assurance Executive Summary: <strong>"
           f"{current_session_g.get('project', '')}</strong> Activity for past 30 days</div><center><br />"
           "<table border=0 width=300 cellpadding=3 cellspacing=2>\n")

    parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "PROJECT", CT.CT_ASCENDING)
    bug_table = xml_array_g

    stats = {
        "change_requests_created": 0, "change_requests_closed": 0, "change_requests_deferred": 0,
        "defect_reports_created": 0, "defect_reports_closed": 0, "defects_deferred": 0,
        "average_lifespan_of_change_requests": 0, "average_lifespan_of_defects": 0,
    }
    mean_cnt = {
        "average_lifespan_of_defects": 0, "average_lifespan_of_change_requests": 0,
    }

    now = datetime.now()
    thirty_days_ago = now - timedelta(days=30)

    def _parse(dt_str):
        try:
            return datetime.strptime(dt_str, CT.CT_DATE_FORMAT)
        except Exception:
            return None

    for bug in bug_table:
        if bug.get("Project") != current_session_g.get("project"):
            continue
        if "Delete_Bug" in bug:
            continue

        submit_time = _parse(bug.get("Submit_Time", ""))
        last_updated = _parse(bug.get("Last_Updated", ""))

        if submit_time and thirty_days_ago <= submit_time <= now:
            if "Change Req" in bug.get("Severity", ""):
                stats["change_requests_created"] += 1
            else:
                stats["defect_reports_created"] += 1

        if last_updated and thirty_days_ago <= last_updated <= now:
            if "Closed" in bug.get("Status", ""):
                if "Change Req" in bug.get("Severity", ""):
                    stats["change_requests_closed"] += 1
                    if submit_time:
                        stats["average_lifespan_of_change_requests"] += (last_updated - submit_time).total_seconds()
                    mean_cnt["average_lifespan_of_change_requests"] += 1
                else:
                    stats["defect_reports_closed"] += 1
                    if submit_time:
                        stats["average_lifespan_of_defects"] += (last_updated - submit_time).total_seconds()
                    mean_cnt["average_lifespan_of_defects"] += 1
            elif "Deferred" in bug.get("Status", ""):
                if "Change Req" in bug.get("Severity", ""):
                    stats["change_requests_deferred"] += 1
                else:
                    stats["defects_deferred"] += 1

    out += (f"<tr><td colspan=2 align=left class='rowEven2'><b>"
            f"{current_session_g.get('project', '')}</b></td></tr>\n")
    rows = 0
    for stat, value in stats.items():
        if mean_cnt.get(stat, 0) != 0:
            row_class = "rowOdd" if rows % 2 else "rowEven"
            out += (f"<tr><td align=right class='{row_class}'>"
                    f"{stat.replace('_', ' ').title()}"
                    f"</td><td class='{row_class}'>"
                    f"{get_stats(value / mean_cnt[stat]) if 'lifespan' in stat else value}"
                    f"</td></tr>\n")
            rows += 1
    out += "</table>\n</center><br /><br />"
    return out


def create_xml_backups():
    slash_style = "\\" if "\\" in os.getcwd() else "/"
    filenames = ["bugs.xml", "users.xml", "projects.xml", "permissions.xml"]

    os.makedirs(CT.CT_XML_BACKUPS_DIRECTORY, exist_ok=True)

    for filename in filenames:
        source = CT.CT_XML_DIRECTORY + slash_style + filename
        destination = (CT.CT_XML_BACKUPS_DIRECTORY + slash_style + filename
                       + datetime.now().strftime("_%m-%d-%Y_%H.%M"))
        try:
            shutil.copy(source, destination)
        except OSError:
            raise RuntimeError(
                f"<br /><br /><center>Fatal: Unable to create backup file '{destination}' "
                f"copied from '{source}'! <br />"
                " Check that the directory is readable and writable by the web server.")


def print_audit_trail(data_array, id_):
    row_cnt = len(data_array)
    last_entry = None
    updates_posted = False
    out = ""

    for i in range(row_cnt):
        if data_array[i].get("ID") == id_:
            if last_entry is None:
                out += ("<div class='txtSmall'>\n<center><br /><b>History for Issue #"
                        f"<a href='esltrack.php?page=viewissue&amp;id={id_}'>{id_}</a>"
                        "</b></center>\n")
                out += "<br />&nbsp;Original Report\n"
                initial_report = dict(data_array[i])
                initial_report.pop("Last_Updated", None)
                out += draw_read_only_table(initial_report)
                last_entry = data_array[i]
                continue

            updates_posted = True

            if "Last_Updated" not in data_array[i]:
                data_array[i]["Last_Updated"] = data_array[i].get("Submit_Time", "")
                data_array[i]["Updated_By"] = "someone"
                data_array[i]["Submit_Time"] = last_entry.get("Submit_Time", "")

            out += ("<br /><hr class='hrBlack' width='80%' align='left'>\n<br />"
                    f"On {data_array[i]['Last_Updated']}, <strong>{data_array[i]['Updated_By']}</strong> "
                    "modified this entry:<br /><br />")

            for column, newval in data_array[i].items():
                newval = _nl2br(str(newval))
                if column in ("Last_Updated", "Updated_By"):
                    continue
                oldval = last_entry.get(column)
                if not oldval:
                    out += f"<b>{column.replace('_', ' ')}</b> \"{newval}\" was <b>added</b>.<br />"

            for column, oldval in last_entry.items():
                if column in ("Last_Updated", "Updated_By"):
                    continue
                newval = data_array[i].get(column)
                if not newval:
                    out += f"<b>{column.replace('_', ' ')}</b> was <b>deleted</b>.<br />"
                elif oldval != newval:
                    out += (f"<b>{column.replace('_', ' ')}</b> \"{oldval}\" "
                            f"<b>changed to</b> \"{newval}\". <br />")
            last_entry = data_array[i]

    out += "<br /><br />\n</div>\n"

    if last_entry is None:
        raise RuntimeError(f"Fatal: No issue #{id_} found!")

    if not updates_posted:
        out += "<em>(No updates have been made to this report)</em>"

    return out


def print_deleted_bugs(bug_table):
    bug_table.sort(key=lambda x: (x.get("Project", "").lower(), x.get("ID", "")))
    row_cnt = len(bug_table)

    out = ""
    if row_cnt:
        out += """
		<center>
		<div class='txtSmall'><br /><b>Deleted Reports by Project</b><br /><br /><br /></div>
		<table border=1 cellpadding=2 cellspacing=0 summary="Deleted Issues List">
		<tr class='rowOdd'><th>ID</th><th>Project</th><th>Original Author</th>
		<th>Submit Time</th><th>Summary</th><th>Deleted On</th><th>Deleted By</th></tr>
"""
        displayed_cnt = 0
        for row in range(row_cnt):
            if bug_table[row].get("Delete_Bug") == "Y":
                cls = "rowOdd" if displayed_cnt % 2 else "rowEven"
                displayed_cnt += 1
                out += ("<tr>"
                        f"\t<td class='{cls}'> <a href='esltrack.php?page=audit&amp;id="
                        f"{bug_table[row].get('ID')}'> {bug_table[row].get('ID')}</a> "
                        f"\t<td class='{cls}'> {bug_table[row].get('Project')} </td>\n"
                        f"\t<td class='{cls}'> {bug_table[row].get('Submitted_By')} </td>\n"
                        f"\t<td class='{cls}'> {bug_table[row].get('Submit_Time')} </td>\n"
                        f"\t<td class='{cls}'> {bug_table[row].get('Summary')} </td>\n"
                        f"\t<td class='{cls}'> {bug_table[row].get('Last_Updated')} </td>\n"
                        f"\t<td class='{cls}'> {bug_table[row].get('Updated_By')} </td>\n"
                        "</tr>\n")
    else:
        out += "<br /><br /><em>(No deleted issues found)</em><br />\n"
    out += "</table></center><br />\n"
    return out


def save_bug_data(id_="", raw_bug_data=None, attachment_data=None, original_submit_time="",
                  send_mail=False, cc_list=None, bug_table=None, user_table=None):
    global current_session_g

    if raw_bug_data is None:
        raw_bug_data = {}
    if cc_list is None:
        cc_list = []

    final_filename = ""
    if attachment_data and attachment_data.get("filename"):
        sanitized_name = process_file_attachment(attachment_data["tmp_name"], attachment_data["filename"])
        final_filename = f"<Attachment>{sanitized_name}</Attachment>"

    b_submittor = b_assignee = b_modifier = b_project = b_summary = b_description = b_severity = ""
    if CT.CT_ENABLE_EMAIL:
        b_submittor = re.sub(r"([^a-z0-9' -]+)", "", raw_bug_data.get("Submitted_By", ""), flags=re.I)
        b_assignee = re.sub(r"([^a-z0-9' -]+)", "", raw_bug_data.get("Assign_To", ""), flags=re.I)
        b_modifier = re.sub(r"([^a-z0-9' -]+)", "", raw_bug_data.get("Modified_By", ""), flags=re.I)
        b_project = re.sub(r"([^a-z0-9' -]+)", "", raw_bug_data.get("Project", ""), flags=re.I)
        b_summary = re.sub(r"([^a-z0-9'/@:\" -]+)", "", raw_bug_data.get("Summary", ""), flags=re.I)
        b_description = re.sub(r"([^a-z0-9'/@:\"\r\n. -]+)", "", raw_bug_data.get("Description", ""), flags=re.I)
        b_severity = re.sub(r"([^a-z0-9' -]+)", "", raw_bug_data.get("Severity", ""), flags=re.I)

    bug_data = scrub_and_tag_form_data(dict(raw_bug_data))

    if original_submit_time:
        submit_time = f"<Submit_Time>{original_submit_time}</Submit_Time>"
        updated_by = f"<Updated_By>{current_session_g.get('user_full_name', '')}</Updated_By>"
    else:
        submit_time = f"<Submit_Time>{datetime.now().strftime(CT.CT_DATE_FORMAT)}</Submit_Time>"
        updated_by = None

    last_updated = f"<Last_Updated>{datetime.now().strftime(CT.CT_DATE_FORMAT)}</Last_Updated>"

    this_is_new_bug = False
    if id_:
        this_is_new_bug = False
    else:
        id_ = calc_next_node_id(bug_table)
        this_is_new_bug = True
    id_tag = "<ID>%04d</ID>" % int(id_)

    # Validate vital fields (strip tags for check)
    def _strip_tag(s):
        m = re.match(r"<[^>]+>(.*)</[^>]+>", s or "")
        return m.group(1) if m else s

    if not (_strip_tag(bug_data.get("Project")) and _strip_tag(bug_data.get("Severity"))
            and _strip_tag(bug_data.get("Summary")) and _strip_tag(bug_data.get("Description"))
            and _strip_tag(bug_data.get("Submitted_By"))):
        raise RuntimeError(
            "<br /><h3><center>Fatal:  Vital form data for create issue not received "
            "(Project/Severity/Summary/Description/Submitted By)!")

    node = f"\t<bug>\n\t\t{id_tag}\n\t\t{submit_time}"
    for content in bug_data.values():
        node += f"\n\t\t{content}"
    if final_filename:
        node += f"\n\t\t{final_filename}"
    node += f"\n\t\t{last_updated}"
    if updated_by:
        node += f"\n\t\t{updated_by}"
    node += "\n\t</bug>"

    append_xml_file_node(CT.CT_XML_BUGS_FILE, node, "</bugs>")

    # Email notification
    if CT.CT_ENABLE_EMAIL and send_mail:
        if b_assignee:
            assignee_email = ""
            for j in range(len(user_table)):
                if user_table[j].get("Full_Name") == b_assignee:
                    assignee_email = user_table[j].get("Email", "")
                    break
            cc_list.append(assignee_email)

        if this_is_new_bug:
            subject = f"New issue (ID# {id_}) has been filed for the {b_project} project by {b_submittor}"
            msg = (f"A {b_severity} issue has been submitted by {b_submittor} for the {b_project} project\r\n"
                   "____________________________________________________________\r\n\r\n"
                   f"Summary: {b_summary} \r\n\r\n"
                   f"Description: {b_description} \r\n\r\n"
                   "____________________________________________________________\r\n\r\n"
                   f"This issue has been assigned to {b_assignee}")
        else:
            subject = f"An update to issue # {id_} on the {b_project} project has been filed by {b_modifier}"
            msg = (f"Issue is currently rated as: {b_severity} \r\n"
                   "____________________________________________________________\r\n\r\n"
                   f"Summary: {b_summary} \r\n\r\n"
                   f"Description: {b_description} \r\n\r\n"
                   "____________________________________________________________\r\n\r\n"
                   f"This issue has been assigned to {b_assignee}.")

        # Mail sending is a no-op stub (no SMTP configured); log instead.
        for recipient in cc_list:
            app.logger.info("Would send mail to %s: %s", recipient, subject)

    issue_action = "created" if this_is_new_bug else "updated"

    return (f"<span class'txtSmall'><center><br /><br /><br /><br /><em>Issue # {id_} has been {issue_action}.</em>"
            "<br /><br /><br /><form action='esltrack.php' method=get>"
            "<input type=hidden name=page value=home><input type=submit value=' OK '></form>"
            "</center></span>\n")


def update_user(user_data, user_table, old_password=""):
    global current_session_g

    user_table.sort(key=lambda x: x.get("ID", "").lower())

    matching_id = -1
    for i in range(len(user_table)):
        if user_table[i].get("ID") == user_data.get("ID"):
            matching_id = i
            break

    if matching_id < 0:
        raise RuntimeError("<br /><br /><center>Fatal:  No matching user ID found!")

    username_to_update = user_table[matching_id].get("Username")

    if current_session_g.get("role") == "Admin":
        next_page = "adminLinks"
    else:
        if username_to_update != current_session_g.get("username"):
            raise RuntimeError(
                "<br /><br /><center> Unless you're an admin, you can only change your own password. ")

        old_hash = user_table[matching_id].get("Password", "")
        old_salt = old_hash[:2]
        import crypt as _crypt
        if _crypt.crypt(old_password, old_salt) != old_hash:
            out = draw_change_password_form(user_table)
            out += ("<br /><center><div class='txtSmall'><b>Password change failed.</b> <br />"
                    "The old password you gave was incorrect. <br />Please try again. </div>\n")
            return out
        next_page = "home"

    if len(user_data.get("Password", "")) < CT.CT_MIN_PASSWORD_LENGTH:
        raise RuntimeError(
            "<br /><br /><center> Password length was less than the minimum "
            f"{CT.CT_MIN_PASSWORD_LENGTH} characters. Changes were NOT saved .")

    user_data["Password"] = encrypt_password(user_data["Password"])

    update_xml_file_node(CT.CT_XML_USERS_FILE, user_table, "user", user_data)

    return ("<br /><br /><br /><br /><center><b><em> Password for "
            f"{user_table[matching_id].get('Full_Name')} successfully changed. </em></b>"
            "<br /><br /><br /><form action='esltrack.php' method=get>"
            f"<input type=hidden name='page' value='{next_page}'><input type=submit value=' OK '></form></center>")


def save_permission_data(permission_data, permission_table, project_table):
    if not permission_data or "Project_ID" not in permission_data:
        raise RuntimeError("<br /><center>Fatal:  Received permission data are corrupt! Nothing was saved.")

    permission_tree = []
    packed_index = 0

    for row in permission_table:
        if row.get("Project_ID") != permission_data["Project_ID"]:
            permission_tree.append({
                "ID": "%04d" % packed_index,
                "Project_ID": row.get("Project_ID"),
                "User_ID": row.get("User_ID"),
            })
            packed_index += 1

    for checked_user_id in permission_data.get("User_ID", []):
        permission_tree.append({
            "ID": "%04d" % packed_index,
            "Project_ID": permission_data["Project_ID"],
            "User_ID": checked_user_id,
        })
        packed_index += 1

    rewrite_xml_file(CT.CT_XML_PERMISSIONS_FILE, permission_tree, "permission")

    return ("<center><br /><br /><br /><div class='txtSmall'>Project permissions successfully saved.<br />\n"
            "<form method=get action=esltrack.php><input type=hidden name='page' value='adminLinks'>\n"
            "<br /><input type=submit value=' OK '></form>")


def save_project_data(project_data, project_table):
    id_ = calc_next_node_id(project_table)
    project_data["ID"] = "%02d" % id_

    project_data["Title"] = re.sub(r"[^A-Z0-9, '\"-]+", "", project_data.get("Title", ""), flags=re.I)
    project_data["Description"] = re.sub(r"[^A-Z0-9 (),-]+", "", project_data.get("Description", ""), flags=re.I)
    project_data["Test_Lead"] = re.sub(r"[^A-Z0-9(),-]+", "", project_data.get("Test_Lead", ""), flags=re.I)

    if not project_data.get("Title") or not project_data.get("Description"):
        raise RuntimeError(
            "<br /><h3><center>Fatal:  Vital form data for create project not received (Title or Description)!")

    out = ("\n\n<br /><br /><br />\n<center><table border=0 cellpadding=3 cellspacing=0 "
           "summary='Project Maintenance'>\n")
    out += ("<tr><td colspan=2 class='newUserSummaryTitle'>"
            " &nbsp; New Project Summary<br />&nbsp;</td></tr>\n\n")

    for field, contents in project_data.items():
        field = field.replace("_", " ")
        out += (f"<tr><td class='newUserSummaryField' align=RIGHT> {field} &nbsp;</td>"
                f"<td class='newUserSummaryInfo' align=LEFT>&nbsp; {contents} &nbsp;</td></tr>\n\n")
    out += "</table>\n"

    project_data = scrub_and_tag_form_data(project_data)

    node = "\t<project>"
    for content in project_data.values():
        node += f"\n\t\t{content}"
    node += "\n\t</project>"

    append_xml_file_node(CT.CT_XML_PROJECTS_FILE, node, "</projects>")

    out += ("<br /><br /><em>This project has been successfully added.</em>"
            "<br /><br /><br /><form action='esltrack.php' method=get>"
            "<input type=hidden name='page' value='adminLinks'><input type=submit value=' OK '></form>"
            "</center>\n")
    return out


def save_user_data(user_data, user_table, notify_user="", apache_vars=None):
    apache_vars = apache_vars or {}
    host = apache_vars.get("HTTP_HOST", "")
    uri = apache_vars.get("REQUEST_URI", "")
    ssl = apache_vars.get("HTTPS", "")

    if not user_data.get("First_Name") or not user_data.get("Last_Name"):
        raise RuntimeError(
            "<br /><h3><center>Fatal:  Vital form data for create user not received (First or Last Name)!")

    if user_data.get("Password"):
        initial_password = user_data["Password"]
    else:
        initial_password = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(14))

    id_ = calc_next_node_id(user_table)
    user_data["ID"] = "%04d" % id_

    user_data["First_Name"] = re.sub(r"[^A-Z0-9,-]+", "", user_data.get("First_Name", ""), flags=re.I)
    user_data["Last_Name"] = re.sub(r"[^A-Z0-9,-]+", "", user_data.get("Last_Name", ""), flags=re.I)

    phone = re.sub(r"[^0-9\-\,\.\+ ]+", "", user_data.get("Phone", ""))
    user_data["Phone"] = phone[:32]

    user_data["Email"] = re.sub(r"[^A-Z0-9@._-]+", "", user_data.get("Email", "").lower(), flags=re.I)

    user_data["Full_Name"] = user_data["First_Name"] + " " + user_data["Last_Name"]
    user_data["Initials"] = (user_data["First_Name"][:1] + user_data["Last_Name"][:1]).upper()

    if user_data.get("Username"):
        user_data["Username"] = re.sub(r"[^a-z0-9._-]+", "", user_data["Username"], flags=re.I)
    else:
        user_data["Username"] = re.sub(
            r"[^a-z0-9._-]+", "",
            (user_data["First_Name"][:1] + user_data["Last_Name"]).lower(), flags=re.I)

    name_list = [u.get("Username") for u in user_table]
    username_accession = 0
    base_name = user_data["Username"]
    while user_data["Username"] in name_list:
        username_accession += 1
        user_data["Username"] = base_name + str(username_accession)

    user_data["Password"] = encrypt_password(initial_password)

    out = ("\n\n<br /><br /><br />\n<center><table border=0 cellpadding=3 cellspacing=0 "
           "summary='New User Confirmation'>\n")
    out += ("<tr><td colspan=2 class='newUserSummaryTitle'>"
            " &nbsp; New User Summary<br />&nbsp;</td></tr>\n\n")

    for field, contents in user_data.items():
        field = field.replace("_", " ")
        display = f"<span id='displayPwd'>{initial_password}</span>" if field == "Password" else contents
        out += (f"<tr><td class='newUserSummaryField' align=RIGHT> {field} &nbsp;</td>"
                f"<td class='newUserSummaryInfo' align=LEFT> &nbsp;{display} &nbsp;</td></tr>\n\n")
    out += "</table>\n"

    msg = ""
    recipient = ""
    if notify_user:
        protocol = "https://" if ssl else "http://"
        url = protocol + host + uri
        msg = ("\nA new account has been set up for you on esltrack. \r\n\r\n"
               f"Your username is: {user_data['Username']}\r\n"
               f"Your initial password is: {initial_password} \r\n\r\n"
               f"Please bookmark this URL: \r\n{url} "
               "to access the system. \r\n\r\n"
               "Also, please verify the spelling and accuracy of the information below. \r\n\r\n"
               f"{user_data['Full_Name']}: {user_data['Role']}")
        recipient = user_data["Email"]

    user_data = scrub_and_tag_form_data(user_data)

    node = "\t<user>"
    for content in user_data.values():
        node += f"\n\t\t{content}"
    node += "\n\t</user>"

    append_xml_file_node(CT.CT_XML_USERS_FILE, node, "</users>")

    if notify_user:
        subject = "New account on the esltrack system"
        app.logger.info("Would send mail to %s: %s", recipient, subject)
        out += f"<br /><br />The new account information has been mailed to {recipient}."

    out += ((f'<br /><br /><strong>Note: This username already existed.  An accession number was added.</strong><br />'
             if username_accession else "")
            + "<br /><br /><em>This user has been successfully added.</em>"
            + "<br /><br /><form action='esltrack.php' method=get>"
            + "<input type=hidden name='page' value='adminLinks'>"
            + "<input type=submit value=' OK '></form></center>\n")
    return out


def search_bugs(pattern, search_options, project_table):
    global xml_array_g

    pattern = re.sub(r"([^ .#&$=0-9a-z_-]+)", " ", pattern, flags=re.I).strip()
    pattern = re.sub(r"[ ]+", " ", pattern)

    if not pattern:
        raise RuntimeError(
            '<br /><br /><center><h3>No search terms were supplied. Enter a keyword or phrase, e.g., "blue screen".')

    if not search_options:
        raise RuntimeError(
            "<br /><br /><center><h3>No search fields specified.  You must choose at least one field (i.e., Summaries).")

    parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
    bug_array = xml_array_g

    if search_options.get("phrase"):
        needles = [pattern]
        title = f"Issues Containing: &nbsp;<em>&quot;{pattern}&quot;</em>"
    else:
        needles = pattern.split(" ")
        title = "Issues Containing: "
        for j in range(len(needles)):
            if j:
                title += f" or <em>&quot;{needles[j]}&quot;</em>"
            else:
                title += f"<em>&quot;{needles[j]}&quot;</em>"
    title += " &nbsp;(Detailed View Below)"

    hits = []
    max_ceiling_hit = False

    for bug in bug_array:
        if "Delete_Bug" in bug:
            continue

        haystack = ""
        if "summary" in search_options:
            haystack = bug.get("Summary", "")
        if "description" in search_options:
            haystack += " " + bug.get("Description", "")
        if "comment" in search_options:
            haystack += " " + bug.get("Developer_Comment", "")

        haystack = haystack.strip()

        for needle in needles:
            if needle.lower() in haystack.lower():
                hits.append(bug)
                break

        if len(hits) == CT.CT_MAX_SEARCH_RESULTS:
            max_ceiling_hit = True
            title += (f"<br /><em>Note: Too many hits for this search term. Showing first "
                      f"{CT.CT_MAX_SEARCH_RESULTS} results.</em>")
            break

    out = ""
    if hits:
        hits.sort(key=lambda x: x.get("ID", "").lower())
        out += draw_table(hits, title, "", project_table)
        out += ("<center><span class='txtSmall'>Reports</span></center>"
                "<hr class='hrBlack' width='80%'><br />\n")
        for hit in hits:
            out += draw_read_only_table(hit)
            out += "<br /> __________________________________________ <br /><br />\n"
    else:
        out += "<br /><br /><center><span class='txtSmall'>Search Results: <em>No matches found</em></span></center>\n"
    return out


def export_database(export_options, user_agent):
    global xml_array_g

    show_history = export_options.get("show_history")
    sort_type = export_options.get("sort_type")
    show_deleted = export_options.get("show_deleted")
    file_format = export_options.get("file_format")
    project_filter = export_options.get("project_filter")

    force_download = False
    if file_format == "xml_download":
        force_download = True
        file_format = "xml"

    table_name = "bugs"
    download_filename = f"{table_name}.{file_format}"

    # XML: push file directly
    if file_format == "xml":
        try:
            with open(CT.CT_XML_BUGS_FILE, "rb") as fh:
                content = fh.read()
        except OSError:
            raise RuntimeError(
                "<br /><center><h3>Fatal: Couldn't open 'CT_XML_BUGS_FILE' ! Check that "
                "it's readable by the web server owner.")
        resp = Response(content, mimetype="application/xml")
        if force_download:
            resp.headers["Content-Disposition"] = f"attachment; filename=\"{download_filename}\""
        return resp

    column_names = parse_dtd_file(f"{table_name}.dtd")
    xml_filename = os.path.join(CT.CT_XML_DIRECTORY, f"{table_name}.xml")

    xml_array_g = []
    if show_history:
        parse_xml_file(xml_filename, CT.CT_KEEP_HISTORY, "ID", CT.CT_ASCENDING)
    else:
        parse_xml_file(xml_filename, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)

    data_array = xml_array_g
    data_array.sort(key=lambda x: (x.get("Project", "").lower(), x.get("ID", "")))

    table_prefix = table_suffix = line_prefix = line_suffix = ""
    delimiter = escaped_delimiter = separator = ""

    if file_format == "csv":
        delimiter = '"'
        escaped_delimiter = '""'
        separator = ","
        line_suffix = "\r\n"
    elif file_format == "prn":
        delimiter = "\t"
        escaped_delimiter = "[tab]"
        separator = ""
        line_suffix = "\r\n"
        table_prefix = "\n"
        table_suffix = "\n\n"
    elif file_format == "sql":
        delimiter = "'"
        escaped_delimiter = "'''"
        separator = ","
        table_prefix = "-- ALTER SESSION SET NLS_DATE_FORMAT='DD-MON-YYYY HH:MI:SS AM' ;\n\n"
        table_suffix = "\n.\n/\n\n"
        line_prefix = "INSERT INTO bugs VALUES ("
        line_suffix = ") ; \r\n"
    else:
        raise RuntimeError("<br /><br /><center>Internal Fatal: No download type specified!")

    out = ""

    if file_format in ("csv", "prn"):
        for j in range(len(column_names)):
            out += (separator if j else "") + delimiter + column_names[j] + delimiter
        out += "\r\n"

    out += table_prefix

    for row in range(len(data_array)):
        if "Delete_Bug" in data_array[row] and data_array[row]["Delete_Bug"] == "Y" and not show_deleted:
            continue
        if project_filter and data_array[row].get("Project") != project_filter:
            continue

        for idx, column in enumerate(column_names):
            cell = reverse_htmlspecialchars(data_array[row].get(column, ""))
            cell = cell.replace("\n", CT.CT_NEWLINE_SYMBOL).replace("\r", CT.CT_NEWLINE_SYMBOL)

            if idx == 0:
                out += line_prefix
                if file_format == "prn":
                    out += cell + delimiter
                else:
                    out += delimiter + cell + delimiter
            else:
                out += separator + delimiter + cell
                if file_format != "prn":
                    out += delimiter
        out += line_suffix

    out += table_suffix

    resp = Response(out, mimetype="application/force-download")
    resp.headers["Content-Disposition"] = f"inline; filename=\"{download_filename}\""
    return resp


# ===========================================================================
#  Request parsing helpers (PHP-style bracket notation)
# ===========================================================================

def _parse_bracket_form(form_dict):
    """Convert Flask's flat form dict into nested dicts using PHP bracket syntax.

    e.g. {'userLogin[username]': 'x'} -> {'userLogin': {'username': 'x'}}
    """
    result = {}
    for key, value in form_dict.items():
        if "[" in key and key.endswith("]"):
            base = key[:key.index("[")]
            inner = key[key.index("[") + 1:-1]
            result.setdefault(base, {})[inner] = value
        else:
            result[key] = value
    return result


def _parse_bracket_args(args_dict):
    """Same as above but for query args (GET)."""
    result = {}
    for key, value in args_dict.items():
        if "[" in key and key.endswith("]"):
            base = key[:key.index("[")]
            inner = key[key.index("[") + 1:-1]
            result.setdefault(base, {})[inner] = value
        else:
            result[key] = value
    return result


# ===========================================================================
#  Main route
# ===========================================================================

@app.route("/esltrack.php", methods=["GET", "POST"])
@app.route("/", methods=["GET", "POST"])
def esltrack():
    global debug_g, current_session_g, xml_array_g

    just_posted = request.method == "POST"

    query_string_g = request.query_string.decode("utf-8", errors="replace")

    debug_g = False
    if CT.CT_ENABLE_AD_HOC_DEBUG and ("debug" in request.args or "debug" in request.form):
        debug_g = True

    xml_array_g = []
    current_session_g = {}

    # Determine page
    page = request.args.get("page") or request.form.get("page")
    if not page:
        return redirect("/esltrack.php?page=login")

    page = re.sub(r"[^a-z]+", "", page, flags=re.I)

    # Session validation for all pages except login/logout
    if page not in ("login", "logout"):
        session_cookie = request.cookies.get("esltrack_session")
        current_session_g = decode_session_cookie(session_cookie) if session_cookie else None

        if current_session_g:
            full_name = current_session_g.get("user_full_name")
            username = current_session_g.get("username")
            valid_sid = calc_session_id(full_name)
            if not valid_sid or valid_sid != current_session_g.get("id"):
                return redirect(f"/esltrack.php?page=login&session=expired&user={username}")
        else:
            referer = request.headers.get("Referer", "")
            if "esltrack.php" in referer:
                return redirect("/esltrack.php?page=login&nocookies=true")
            else:
                return redirect("/esltrack.php?page=login&session_authentication=missing")

    # Load users
    parse_xml_file(CT.CT_XML_USERS_FILE, CT.CT_DROP_HISTORY, "Last_Name", CT.CT_ASCENDING)
    user_table = [u for u in xml_array_g if u.get("Phone") != "_REMOVED_"]
    random.shuffle(user_table)

    # Load permissions
    parse_xml_file(CT.CT_XML_PERMISSIONS_FILE, CT.CT_DROP_HISTORY, "Project_ID", CT.CT_ASCENDING)
    permission_table = xml_array_g

    # Load projects
    parse_xml_file(CT.CT_XML_PROJECTS_FILE, CT.CT_DROP_HISTORY, "Title", CT.CT_ASCENDING)
    project_table = xml_array_g
    project_cnt = len(project_table)

    # ------------------------------------------------------------------
    # Main switch
    # ------------------------------------------------------------------
    try:
        if page == "login":
            if just_posted:
                form = _parse_bracket_form(request.form)
                user_login = form.get("userLogin", {})
                match, index = valid_password(user_login, user_table)
                if match:
                    user_entry = user_table[index]
                    selected_project = user_login.get("project_name")
                    cookie = build_new_session_cookie(user_entry, selected_project)
                    if user_login.get("password") == "esltrack":
                        return set_session_cookie_load_page(cookie, "changepassword")
                    else:
                        return set_session_cookie_load_page(cookie, "home")
                else:
                    return redirect("/esltrack.php?page=login&failed=y")
            else:
                prev_login_failed = "failed" in request.args
                session_expired = "session" in request.args
                no_cookies = "nocookies" in request.args
                out = draw_login_page(project_table, prev_login_failed, session_expired, no_cookies)
                return _no_cache_response(Response(out))

        elif page in ("logout", "timeout"):
            extra = "&session=expired" if page == "timeout" else ""
            out = ("<html><body><script type='text/javascript'> "
                   "document.cookie=\"esltrack_session=;\";  "
                   f"location.replace('esltrack.php?page=login{extra}');</script></body></html>")
            return Response(out)

        elif page == "newIssue":
            out = draw_page_top(page) + draw_add_edit_bug_form(project_table, user_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "changepassword":
            out = draw_page_top(page) + draw_change_password_form(user_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "searchissue":
            pattern = request.args.get("pattern", "")
            search_options = _parse_bracket_args(request.args).get("search_options", {})
            out = draw_page_top(page) + search_bugs(pattern, search_options, project_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "savepassword":
            if just_posted:
                form = _parse_bracket_form(request.form)
                user_data = form.get("user_data", {})
                old_pw = request.form.get("old_pw", "")
                out = draw_page_top(page) + update_user(user_data, user_table, old_pw) + draw_page_bottom()
                return _no_cache_response(Response(out))
            return redirect("/esltrack.php?page=home")

        elif page == "saveissue":
            if just_posted:
                attachment_data = None
                if "Attachment" in request.files and request.files["Attachment"].filename:
                    f = request.files["Attachment"]
                    tmp_path = os.path.join("/tmp", f.filename)
                    f.save(tmp_path)
                    attachment_data = {"tmp_name": tmp_path, "filename": f.filename}

                form = _parse_bracket_form(request.form)
                id_ = request.form.get("id", "")
                bug_data = form.get("bug_data", {})
                original_submit_time = request.form.get("original_submit_time", "")
                send_mail = request.form.get("send_mail", "")
                cc_list = request.form.getlist("cc_list[]")

                xml_array_g = []
                parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
                bug_table = xml_array_g

                out = draw_page_top(page) + save_bug_data(
                    id_, bug_data, attachment_data, original_submit_time,
                    send_mail, cc_list, bug_table, user_table) + draw_page_bottom()
                return _no_cache_response(Response(out))
            return redirect("/esltrack.php?page=home")

        elif page == "export":
            if just_posted:
                form = _parse_bracket_form(request.form)
                export_options = form.get("export_options", {})
                user_agent = request.headers.get("User-Agent", "")
                return export_database(export_options, user_agent)
            else:
                out = draw_page_top(page) + draw_export_options(current_session_g.get("project", "")) + draw_page_bottom()
                return _no_cache_response(Response(out))

        elif page == "tools":
            out = draw_page_top(page) + draw_tools_page() + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "summary":
            out = draw_page_top(page) + create_summary(project_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "deletedissues":
            parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
            bug_table = xml_array_g
            out = draw_page_top(page) + print_deleted_bugs(bug_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "audit":
            id_ = request.args.get("id")
            if not id_:
                raise RuntimeError("Fatal: No issue id specified for audit record!")
            parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_KEEP_HISTORY)
            data_array = xml_array_g
            out = draw_page_top(page) + print_audit_trail(data_array, str(id_)) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "viewissue":
            id_ = request.args.get("id")
            if id_:
                out = draw_page_top(page) + draw_view_bug_page(str(id_)) + draw_page_bottom()
                return _no_cache_response(Response(out))
            else:
                raise RuntimeError("<center><br /><br /><h2>Fatal: No issue ID specified to view!")

        elif page == "editissue":
            id_ = request.args.get("id")
            if id_:
                out = draw_page_top(page) + draw_add_edit_bug_form(project_table, user_table, str(id_)) + draw_page_bottom()
                return _no_cache_response(Response(out))
            else:
                raise RuntimeError("<center><br /><br /><h2>Fatal: No issue ID specified to edit!")

        elif page == "users":
            out = draw_page_top(page) + draw_table(user_table, "Address Book", "") + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "projects":
            out = draw_page_top(page) + draw_table(project_table, "Active Projects", "") + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "changeproject":
            redir = request.args.get("redir", "")
            project = request.args.get("project", "")
            cookie = build_updated_session_cookie(project, current_session_g)
            return set_session_cookie_load_page(cookie, redir)

        elif page == "home":
            project = request.args.get("project")
            all_ = request.args.get("all")

            parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
            bug_array = xml_array_g

            filter_ = {}
            if project:
                filter_["Project"] = project
            else:
                filter_["Project"] = current_session_g.get("project", "")

            t = "title='Show me only the open issues'"
            if all_:
                title = f"All <a href='esltrack.php?page=home' {t}>{filter_['Project']}</a> Issues"
            else:
                filter_["Status"] = "Open"
                title = f"Open <a href='esltrack.php?page=home&amp;all=1' {t}>{filter_['Project']}</a> Issues"

            out = draw_page_top(page) + draw_table(bug_array, title, filter_, project_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "filter":
            filter_ = _parse_bracket_args(request.args).get("filter", {})

            parse_xml_file(CT.CT_XML_BUGS_FILE, CT.CT_DROP_HISTORY, "ID", CT.CT_ASCENDING)
            bug_array = xml_array_g

            report_title = ""
            for filter_name, filter_value in list(filter_.items()):
                if not filter_value:
                    del filter_[filter_name]
                else:
                    if not report_title:
                        report_title = "Custom Report:<br /><em>All issues matching criteria: &nbsp;"
                    else:
                        report_title += ", &nbsp;"
                    report_title += (filter_name.replace("_", " ")
                                     + ("" if "Submitted_By" in filter_name else " = ")
                                     + f" {filter_value}")
            if not report_title:
                report_title = "Custom Report: <em>Issues from All Projects"

            out = draw_page_top(page) + draw_table(bug_array, f"{report_title}</em>", filter_, project_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "reports":
            out = draw_page_top(page) + draw_reports_page(project_table, user_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        # Admin-only pages
        elif page == "dobackup":
            admin_check()
            create_xml_backups()
            return redirect("/esltrack.php?page=backup&success=1")

        elif page == "adminLinks":
            admin_check()
            out = draw_page_top(page) + draw_admin_page() + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "projectaccess":
            admin_check()
            out = draw_page_top(page) + draw_project_access_form(project_table, user_table, permission_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "backup":
            admin_check()
            success = request.args.get("success")
            out = draw_page_top(page) + draw_xml_backups_page(success) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "savepermissions":
            admin_check()
            form = _parse_bracket_form(request.form)
            permission_data = form.get("permission_data", {})
            # Handle User_ID[] list
            user_ids = request.form.getlist("permission_data[User_ID][]")
            if not user_ids:
                user_ids = request.form.getlist("permission_data[User_ID][0]")
            permission_data["User_ID"] = user_ids
            out = draw_page_top(page) + save_permission_data(permission_data, permission_table, project_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "adduser":
            admin_check()
            out = draw_page_top(page) + draw_user_maintenance_form() + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "addproject":
            admin_check()
            out = draw_page_top(page) + draw_project_maintenance_form(user_table) + draw_page_bottom()
            return _no_cache_response(Response(out))

        elif page == "saveuser":
            admin_check()
            if just_posted:
                form = _parse_bracket_form(request.form)
                user_data = form.get("user_data", {})
                notify_user = request.form.get("notify_user", "")
                apache_vars = {
                    "HTTP_HOST": request.host,
                    "REQUEST_URI": request.full_path,
                    "HTTPS": "on" if request.is_secure else "",
                }
                out = draw_page_top(page) + save_user_data(user_data, user_table, notify_user, apache_vars) + draw_page_bottom()
                return _no_cache_response(Response(out))
            return redirect("/esltrack.php?page=adminLinks")

        elif page == "saveproject":
            admin_check()
            if just_posted:
                form = _parse_bracket_form(request.form)
                project_data = form.get("project_data", {})
                out = draw_page_top(page) + save_project_data(project_data, project_table) + draw_page_bottom()
                return _no_cache_response(Response(out))
            return redirect("/esltrack.php?page=adminLinks")

        else:
            # Generic bit bucket for bogus page request
            ref = _htmlspecialchars(request.headers.get("Referer", ""))
            out = ('<html><body><meta http-equiv="refresh" content="6; url='
                   f'esltrack.php?page=login&invalidpage={page}"><center><h4><br /><br /><br />'
                   f"Invalid page requested '{page}'<br /><br />Referring page: {ref} <em><br /><br />"
                   "Redirecting to login page in six seconds. </em></center></h4></body></html>")
            return _no_cache_response(Response(out))

    except RuntimeError as e:
        return Response(str(e), status=200)


# ===========================================================================
#  Entry point
# ===========================================================================

if __name__ == "__main__":
    # Ensure required directories exist
    for d in (CT.CT_XML_DIRECTORY, CT.CT_XML_BACKUPS_DIRECTORY, CT.CT_ATTACHMENTS_DIRECTORY):
        os.makedirs(d, exist_ok=True)
    app.run(host="0.0.0.0", port=8080, debug=True)
