#!/opt/support/venv/bin/python3
"""Wrapper for /usr/local/cpanel/scripts/restorepkg"""
import os
import argparse
from dataclasses import dataclass
from argparse import ArgumentTypeError as BadArg
from pathlib import Path
import subprocess
import sys
import time
from typing import IO
from collections.abc import Generator
from cpapis import whmapi1, CpAPIError
from cproc import Proc
from netaddr import IPAddress
import rads

sys.path.insert(0, '/opt/support/lib')
import arg_types
from arg_types import CPMOVE_RE
from server import MAIN_RESELLER, ROLE


if Path('/opt/sharedrads/hostsfilemods').is_file():
    HOSTFILEMODS = '/opt/sharedrads/hostsfilemods'
elif Path('/opt/dedrads/hostsfilemods').is_file():
    HOSTFILEMODS = '/opt/dedrads/hostsfilemods'
else:
    HOSTFILEMODS = None


@dataclass
class Args:
    """Type hint for get_args"""

    newuser: str | None
    owner: str
    quiet: bool
    yes: bool
    host_mods: bool
    ipaddr: IPAddress | None
    package: str | None
    fixperms: bool
    path: Path
    log_dir: Path


def get_args() -> Args:
    """Parse sys.argv"""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        '--newuser',
        dest='newuser',
        type=arg_types.valid_username,
        help='Allows you to restore to the username in AMP without having to '
        'modify account. Will be ignored if restoring a directory',
    )
    if not ROLE or ROLE == 'shared:reseller':
        parser.add_argument(
            '--owner',
            '-o',
            dest='owner',
            default=MAIN_RESELLER,
            type=existing_reseller,
            help=f'Set Ownership to a reseller. Defaults to {MAIN_RESELLER}',
        )
    parser.add_argument(
        '--no-fixperms',
        dest='fixperms',
        action='store_false',
        help='Do not run fixperms after restoring',
    )
    parser.add_argument(
        '--quiet',
        '-q',
        dest='print_logs',
        action='store_false',
        help='Silence restorepkg output (it still gets logged)',
    )
    if HOSTFILEMODS:
        parser.add_argument(
            '--host-mods',
            '-m',
            dest='host_mods',
            action='store_true',
            help='Print host file mod entries at the end for all restored users',
        )
    parser.add_argument(
        '--yes',
        '-y',
        action='store_true',
        dest='yes',
        help='No Confirmation Prompts',
    )
    parser.add_argument(
        '--ip',
        dest='ipaddr',
        type=arg_types.ipaddress,
        help='Set an IP address',
    )
    packages = {
        x.name for x in Path('/var/cpanel/packages').iterdir() if x.is_file()
    }
    parser.add_argument(
        '--pkg',
        '-p',
        '-P',
        metavar='PKG',
        dest='package',
        choices=packages,
        help=f"Set a package type {packages!r}",
    )
    parser.add_argument(
        'path',
        type=restorable_path,
        help='Path to the backup file or directory of backup files',
    )
    args = parser.parse_args()
    if args.path.is_dir():
        if args.newuser:
            parser.print_help()
            sys.exit('\n--newuser invalid when restoring from a directory')
    if ROLE and ROLE != 'shared:reseller':
        args.owner = MAIN_RESELLER
    if not HOSTFILEMODS:
        args.host_mods = False
    if ROLE:
        args.log_dir = Path('/home/t1bin')
    else:  # v/ded
        args.log_dir = Path('/var/log/t1bin')
    return args


def existing_reseller(user: str) -> str:
    """Argparse type: validate a user as existing with reseller permissions"""
    if not user:
        raise BadArg('cannot be blank')
    if user == 'root':
        return user
    if not rads.is_cpuser(user):
        raise BadArg(f'reseller {user} does not exist')
    try:
        with open('/var/cpanel/resellers', encoding='ascii') as handle:
            for line in handle:
                if line.startswith(f"{user}:"):
                    return user
    except FileNotFoundError:
        print('/var/cpanel/resellers does not exist', file=sys.stderr)
    raise BadArg(f"{user} not setup as a reseller")


def restorable_path(str_path: str) -> Path:
    """Argparse type: validates a path as either a cpmove file or a
    directory in /home"""
    try:
        return arg_types.cpmove_file_type(str_path)
    except BadArg:
        pass
    try:
        path = arg_types.path_in_home(str_path)
    except BadArg as exc:
        raise BadArg(
            "not a cPanel backup or directory in /home containing them"
        ) from exc
    if path == Path('/home'):
        # it would work, but it's generally a bad idea
        raise BadArg(
            "invalid path; when restoring from a directory, "
            "it must be a subdirectory of /home"
        )
    return path


def log_print(handle: IO, msg: str, show: bool = True):
    """Writes to a log and prints to stdout"""
    if not msg.endswith('\n'):
        msg = f"{msg}\n"
    handle.write(msg)
    if show:
        print(msg, end='')


def set_owner(log_file: IO, user: str, owner: str) -> bool:
    """Change a user's owner"""
    if owner == 'root':
        return True
    log_print(log_file, f'setting owner of {user} to {owner}')
    try:
        whmapi1.set_owner(user, owner)
    except CpAPIError as exc:
        log_print(log_file, f"modifyacct failed: {exc}")
        return False
    return True


def set_ip(log_file: IO, user: str, ipaddr: IPAddress | None) -> bool:
    """Set a user's IP"""
    if not ipaddr:
        return True
    log_print(log_file, f"setting IP of {user} to {ipaddr}")
    try:
        whmapi1.setsiteip(user, str(ipaddr))
    except CpAPIError as exc:
        log_print(log_file, f"setsiteip failed: {exc}")
        return False
    return True


def set_package(log_file: IO, user: str, pkg: str | None) -> bool:
    """Set a user's cPanel package"""
    if not pkg:
        return True
    log_print(log_file, f"setting package of {user} to {pkg}")
    try:
        whmapi1.changepackage(user, pkg)
    except CpAPIError as exc:
        log_print(log_file, f"changepackage failed: {exc}")
        return False
    return True


def restorepkg(
    log_file: IO, cpmove: Path, newuser: str | None, print_logs: bool
):
    """Execute restorepkg"""
    cmd = ['/usr/local/cpanel/scripts/restorepkg', '--skipres']
    if newuser:
        cmd.extend(['--newuser', newuser])
    cmd.append(cpmove)
    success = True
    with Proc(
        cmd,
        lim=os.cpu_count(),
        encoding='utf-8',
        errors='replace',
        stdout=Proc.PIPE,
        stderr=Proc.STDOUT,
    ) as proc:
        for line in proc.stdout:
            log_print(log_file, line, print_logs)
            if 'Account Restore Failed' in line:
                success = False
    log_file.write('\n')
    if proc.returncode != 0:
        log_print(log_file, f'restorepkg exit code was {proc.returncode}')
        success = False
    return success


def restore_user(args: Args, cpmove: Path, user: str, log: Path) -> list[str]:
    """Restore a user (restorepkg + set owner/ip/package) and return a list of
    task(s) that failed, if any"""
    user = args.newuser or user
    if args.owner == user:
        print(f'{args.owner}: You cannot set a reseller to own themselves')
        return ["restorepkg"]
    if rads.is_cpuser(user):
        print(user, 'already exists', file=sys.stderr)
        return ["restorepkg"]
    print('Logging to:', log)
    with log.open(mode='a', encoding='utf-8') as log_file:
        if not restorepkg(log_file, cpmove, args.newuser, args.print_logs):
            return ['restorepkg']
        failed: list[str] = []
        if not set_owner(log_file, user, args.owner):
            failed.append(f'set owner to {args.owner}')
        if not set_ip(log_file, user, args.ipaddr):
            failed.append(f'set ip to {args.ipaddr}')
        if not set_package(log_file, user, args.package):
            failed.append(f'set package to {args.package}')
    return failed


def iter_backups(path: Path) -> Generator[tuple[str, Path], None, None]:
    """Iterate over backups found in a directory"""
    for entry in path.iterdir():
        if match := CPMOVE_RE.match(entry.name):
            yield match.group(1), entry


def main():
    """Wrapper around cPanel's restorepkg"""
    args = get_args()
    user_fails: dict[str, list[str]] = {}  # user: list of any tasks that failed
    args.log_dir.mkdir(mode=770, exist_ok=True)
    if args.path.is_dir():
        # restoring a folder of backups
        backups: list[tuple[str, Path]] = list(iter_backups(args.path))
        if not backups:
            sys.exit(f'No backups in {args.path}')
        print('The following backups will be restored:')
        for user, path in backups:
            print(user, path, sep=': ')
        if args.yes:
            time.sleep(3)
        else:
            if not rads.prompt_y_n('Would you like to proceed?'):
                sys.exit(0)
        for user, path in backups:
            log = args.log_dir.joinpath(f"{user}.restore.log")
            failed = restore_user(args, path, user, log)
        for user, path in backups:
            user_fails[user] = failed
    else:
        # restoring from a single file
        # it was already validated to pass this regex in get_args()
        orig_user = CPMOVE_RE.match(args.path.name).group(1)
        user = args.newuser if args.newuser else orig_user
        log = args.log_dir.joinpath(f"{user}.restore.log")
        user_fails[user] = restore_user(args, args.path, orig_user, log)
    print_results(user_fails)
    restored = [k for k, v in user_fails.items() if v != ['restorepkg']]
    if args.fixperms:
        fixperms(restored)
    if args.host_mods:
        print_host_mods(restored)


def print_results(user_fails: dict[str, list[str]]):
    """Print results from each ``restore_user()``"""
    print('== Restore Results ==')
    for user, fails in user_fails.items():
        if fails:
            print(user, 'failed', sep=': ', end=': ')
            print(*fails, sep=', ')
        else:
            print(user, 'success', sep=': ')


def fixperms(restored: list[str]):
    """Runs fixperms on restored users"""
    if not restored:
        return
    # fixperms all users in one run and only print errors
    subprocess.call(['/usr/bin/fixperms', '--quiet'] + restored)


def print_host_mods(restored: list[str]):
    """Runs the command at ``HOSTFILEMODS``"""
    print('Host file mod entries for all restored cPanel users:')
    for user in restored:
        subprocess.call([HOSTFILEMODS, user])


if __name__ == "__main__":
    main()
