#!/usr/lib/rads/venv/bin/python3
# vim: set ts=4 sw=4 expandtab syntax=python:
# the dash in this script's filename sets off invalid-name
# pylint: disable=invalid-name
"""Change plan eligibility checker tool"""
from pathlib import Path
import sys
import json
import logging
from argparse import ArgumentParser
from cpapis import uapi, cpapi2, CpAPIError

__version__ = "1.1.0"
__date__ = "08 Nov 2021"

logger = logging.getLogger('upgrade-check')


def setup_logging(clevel=logging.INFO, flevel=logging.DEBUG, logfile=None):
    """
    Setup logging
    """
    logger.setLevel(logging.DEBUG)

    # Console
    con = logging.StreamHandler()
    con.setLevel(clevel)
    con_format = logging.Formatter("%(levelname)s: %(message)s")
    con.setFormatter(con_format)
    logger.addHandler(con)

    # File
    if logfile:
        try:
            flog = logging.handlers.WatchedFileHandler(logfile)
            flog.setLevel(flevel)
            flog_format = logging.Formatter(
                "[%(asctime)s] %(name)s: %(levelname)s: %(message)s"
            )
            flog.setFormatter(flog_format)
            logger.addHandler(flog)
        except Exception as e:
            logger.warning("Failed to open logfile: %s", str(e))


def parse_cli(show_help=False):
    """Parse CLI arguments"""
    parser = ArgumentParser(description="Change plan eligibility checker tool")
    # fmt: off
    parser.set_defaults(user=None, showmail=False)
    parser.add_argument('user', metavar="USER", help="Username to check")
    parser.add_argument(
        '--plan', '-p', metavar="PLAN", help="Plan to check against"
    )
    parser.add_argument(
        '--showmail', '-m', action='store_true',
        help="Show detailed email usage",
    )
    parser.add_argument(
        '--debug', '-d', dest='loglevel',
        action='store_const', const=logging.DEBUG,
        help="Enable debug output",
    )
    parser.add_argument(
        '--version', '-v', action='version',
        version=f"{__version__} ({__date__})",
    )
    # fmt: on
    if show_help:
        parser.print_help()
        sys.exit(1)
    return parser.parse_args()


def format_size(insize, rate=False, bits=False):
    """format human-readable file size and xfer rates"""
    onx = float(abs(insize))
    for u in ['B', 'K', 'M', 'G', 'T', 'P']:
        if onx < 1024.0:
            tunit = u
            break
        onx /= 1024.0
    suffix = ""
    if tunit != 'B':
        suffix = "iB"
    if rate:
        if bits:
            suffix = "bps"
            onx *= 8.0
        else:
            suffix += "/sec"
    if tunit == 'B':
        ostr = "%3d %s%s" % (onx, tunit, suffix)
    else:
        ostr = f"{onx:3.01f} {tunit}{suffix}"
    return ostr


def mkpct(ival, tot):
    """Make value/percent string"""
    try:
        po = f"{ival:d} ({(float(ival) / float(tot)) * 100.0:3.01f}%)"
    except Exception:
        po = "0 (-.-%)"
    return po


def get_account_summary(username):
    """Call UAPI to get domain, DB, and other usage info"""
    try:
        ret = uapi('DomainInfo::list_domains', user=username, check=True)
    except CpAPIError as exc:
        logger.error(str(exc))
        return None
    return ret['result']['data']


def get_db_count(username):
    """Get mySQL database count"""
    try:
        with open(
            Path('/var/cpanel/datastore', username, 'mysql-db-count'),
            encoding='ascii',
        ) as file:
            dcount = int(file.read().strip())
    except Exception as e:
        logger.error("Failed to get DB count: %s", str(e))
        return None
    return dcount


def get_disk_usage(username):
    """Check filesystem quotas for usage"""
    try:
        ret = cpapi2('DiskUsage::fetchdiskusage', user=username)
    except CpAPIError as exc:
        logger.error(str(exc))
        return None
    try:
        usage = int(ret['cpanelresult']['data'][0]['contained_usage'] / 2**20)
    except Exception as exc:
        logger.error("Failed to parse cpapi2 result: %s", exc)
        return None
    return usage


def get_mail_accounts(username):
    try:
        with open(
            Path('/home', username, '.cpanel/email_accounts.json'),
            encoding='ascii',
        ) as file:
            eraw = file.read().strip()
            if len(eraw) > 1:
                ejson = json.loads(eraw)
            else:
                ejson = {}
                logger.debug(
                    "email_accounts.json is empty. assuming 0 accounts."
                )
    except Exception as exc:
        logger.error("failed to open email_accounts.json for user: %s", exc)
        return None

    accounts = []
    for tdomain in ejson.keys():
        if tdomain.startswith('_'):
            continue

        for taccount, tinfo in ejson[tdomain]['accounts'].items():
            accounts.append(
                {
                    'suspended': tinfo['suspended_login'],
                    'domain': tdomain,
                    'user': taccount,
                    'email': f"{taccount}@{tdomain}",
                    'used': int(tinfo['diskused']),
                    'quota': int(tinfo.get('diskquota', 0)),
                }
            )
    return accounts


def print_accounts(accounts):
    """Print list of email accounts by size"""
    print("\n************ Email Accounts ************")
    print("{:40} {:>10} / {:>10}".format("Email", "DiskUsed", "Quota"))

    for taccount in accounts:
        print(
            "{:40} {:>10} / {:>10}".format(
                taccount['email'],
                format_size(taccount['used']),
                format_size(taccount['quota']),
            )
        )


def get_plan_params():
    """Read upgrade_params from file ./etc/upgrade_params.json"""
    ppath = Path(__file__).resolve().parent / 'etc/upgrade_params.json'
    try:
        with open(ppath, encoding='ascii') as f:
            params = json.load(f)['packages']
    except Exception as e:
        logger.error("Failed to read account params from file: %s", str(e))
        return None
    return params


def get_account_stats(domlist, dbcount, maillist, acctsize):
    """Calculate account stats from gathered params"""
    if len(maillist) > 0:
        maxmailsize = int(
            max(x['used'] for x in maillist if not x['suspended'])
            / (1024 * 1024)
        )
    else:
        maxmailsize = 0

    account = {
        'disk': acctsize,
        'db': dbcount,
        'addon': len(domlist['addon_domains']),
        'parked': len(domlist['parked_domains']),
        'sub': len(domlist['sub_domains']),
        'mailacct': len([x for x in maillist if not x['suspended']]),
        'maxmailsize': maxmailsize,
    }
    return account


def check_plan(stat, plan):
    """Compare user stats versus plandata"""
    return (
        _fits_plan(stat['disk'], plan['quota'])
        and _fits_plan(stat['db'], plan['maxdb'])
        and _fits_plan(stat['addon'], plan['maxaddon'])
        and _fits_plan(stat['addon'], plan['maxaddon'])
        and _fits_plan(stat['parked'], plan['maxpark'])
        and _fits_plan(stat['sub'], plan['maxsub'])
        and _fits_plan(stat['mailacct'], plan['maxpop'])
        and _fits_plan(stat['maxmailsize'], plan['max_emailacct_quota'])
    )


def _fits_plan(value, quota):
    if quota == -1:
        return True
    return value <= quota


def check_one_plan(astat, plan):
    """Check a single plan and return yes/no"""
    try:
        plandata = PLANS[plan]
    except Exception:
        logger.error("Plan '%s' does not exist", plan)
        return None

    if check_plan(astat, plandata):
        print("yes")
        return True
    print("no")
    return False


def yesno(ibool):
    """Colorize yes/no from true/false"""
    if ibool:
        return "\033[92mYES\033[0m"
    return "\033[91mNO\033[0m"


def check_all_plans(username, astat):
    """Print a table of all compatible plans"""
    print(f"** Usage summary for user '{username}' ***")
    for tkey, tval in astat.items():
        print(f"{tkey:16} {tval}")

    print("")
    print(f"** Plan compatibility for user '{username}' ***")
    for tplan, plandata in PLANS.items():
        print(f"{tplan:30} {yesno(check_plan(astat, plandata))}")


def main():
    """Entry point"""
    setup_logging()
    args = parse_cli()
    domains = get_account_summary(args.user)
    dbcount = get_db_count(args.user)
    mail_accounts = get_mail_accounts(args.user)
    diskuse = get_disk_usage(args.user)

    if (
        domains is None
        or dbcount is None
        or mail_accounts is None
        or diskuse is None
    ):
        logger.error(
            "Failed to gather data for user. Ensure username is "
            "correct or check account manually."
        )
        sys.exit(2)

    astat = get_account_stats(domains, dbcount, mail_accounts, diskuse)

    if args.plan:
        check_one_plan(astat, args.plan)
    else:
        check_all_plans(args.user, astat)

    if mail_accounts is None:
        logger.error("Failed to get list of email accounts for user")
    else:
        if args.showmail:
            print_accounts(mail_accounts)


PLANS = get_plan_params()

if __name__ == '__main__':
    main()
