# SPDX-License-Identifier: MIT
"""s2idle analysis tool"""
import argparse
import sys
import os
import subprocess
import sqlite3

from datetime import date, timedelta, datetime
from amd_debug.common import (
    convert_string_to_bool,
    colorize_choices,
    is_root,
    relaunch_sudo,
    show_log_info,
    version,
    running_ssh,
)

from amd_debug.validator import SleepValidator
from amd_debug.installer import Installer
from amd_debug.prerequisites import PrerequisiteValidator
from amd_debug.sleep_report import SleepReport, confirm_overwrite_report


class Defaults:
    """Default values for the script"""

    duration = 10
    wait = 4
    count = 1
    since = date.today() - timedelta(days=60)
    until = date.today() + timedelta(days=1)
    format_choices = ["txt", "md", "html", "stdout"]
    boolean_choices = ["true", "false"]


class Headers:
    """Headers for the script"""

    DurationDescription = "How long should suspend cycles last (seconds)"
    WaitDescription = "How long to wait in between suspend cycles (seconds)"
    CountDescription = "How many suspend cycles to run"
    SinceDescription = "What date to start report data"
    UntilDescription = "What date to end report data"
    LogDescription = "Location of log file"
    ReportFileDescription = "Location of report file"
    FormatDescription = "What format to output the report in"
    MaxDurationDescription = "What is the maximum suspend cycle length (seconds)"
    MaxWaitDescription = "What is the maximum time between suspend cycles (seconds)"
    ReportDebugDescription = "Enable debug output in report (increased size)"


def display_report_file(fname, fmt) -> None:
    """Display report file"""
    if fmt != "html":
        return
    if not is_root():
        subprocess.call(["xdg-open", fname])
        return
    user = os.environ.get("SUDO_USER")
    if user:
        env_vars = []
        for var in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR",
                    "DBUS_SESSION_BUS_ADDRESS", "XDG_SESSION_TYPE", "XDG_DATA_DIRS", "KDE_SESSION_VERSION"]:
            value = os.environ.get(var)
            if value:
                env_vars.append(f"{var}={value}")

        if env_vars:
            cmd = ["sudo", "-u", user] + [f"env"] + env_vars + ["xdg-open", fname]
            subprocess.call(cmd)
        else:
            print(
                "Unable to detect graphical session environment. "
                "Report saved to: " + fname
            )


def get_report_file(report_file, extension) -> str:
    """Prompt user for report file"""
    if extension == "stdout":
        return ""
    if not report_file:
        return f"amd-s2idle-report-{date.today()}.{extension}"
    return report_file


def get_report_format() -> str:
    """Get report format"""
    if running_ssh():
        return "txt"
    return "html"


def prompt_report_arguments(since, until, fname, fmt, report_debug) -> list:
    """Prompt user for report configuration"""
    if not since:
        default = Defaults.since
        since = input(f"{Headers.SinceDescription} (default {default})? ")
        if not since:
            since = default.isoformat()
    try:
        since = datetime.fromisoformat(since)
    except ValueError as e:
        sys.exit(f"Invalid date, use YYYY-MM-DD: {e}")
    if not until:
        default = Defaults.until
        until = input(f"{Headers.SinceDescription} (default {default})? ")
        if not until:
            until = default.isoformat()
    try:
        until = datetime.fromisoformat(until)
    except ValueError as e:
        sys.exit(f"Invalid date, use YYYY-MM-DD: {e}")

    if not fmt:
        fmt = input(
            f"{Headers.FormatDescription} ({colorize_choices(Defaults.format_choices, get_report_format())})? "
        )
        if not fmt:
            fmt = get_report_format()
        if fmt not in Defaults.format_choices:
            sys.exit(f"Invalid format: {fmt}")
    if report_debug is None:
        inp = (
            input(
                f"{Headers.ReportDebugDescription} ({colorize_choices(Defaults.boolean_choices, 'true')})? "
            )
            .lower()
            .capitalize()
        )
        report_debug = True if not inp else convert_string_to_bool(inp)
    return [since, until, get_report_file(fname, fmt), fmt, report_debug]


def prompt_test_arguments(duration, wait, count, rand) -> list:
    """Prompt user for test configuration"""
    if not duration:
        if rand:
            question = Headers.MaxDurationDescription
        else:
            question = Headers.DurationDescription
        duration = input(f"{question} (default {Defaults.duration})? ")
        if not duration:
            duration = Defaults.duration
    try:
        duration = int(duration)
    except ValueError as e:
        sys.exit(f"Invalid duration: {e}")
    if not wait:
        if rand:
            question = Headers.MaxWaitDescription
        else:
            question = Headers.WaitDescription
        wait = input(f"{question} (default {Defaults.wait})? ")
        if not wait:
            wait = Defaults.wait
    try:
        wait = int(wait)
    except ValueError as e:
        sys.exit(f"Invalid wait: {e}")
    if not count:
        count = input(f"{Headers.CountDescription} (default {Defaults.count})? ")
        if not count:
            count = Defaults.count
    try:
        count = int(count)
    except ValueError as e:
        sys.exit(f"Invalid count: {e}")
    return [duration, wait, count]


def report(since, until, fname, fmt, tool_debug, report_debug) -> bool:
    """Generate a report from previous sleep cycles"""
    try:
        since, until, fname, fmt, report_debug = prompt_report_arguments(
            since, until, fname, fmt, report_debug
        )
    except KeyboardInterrupt:
        sys.exit("\nReport generation cancelled")
    if not confirm_overwrite_report(fname):
        print("Report generation cancelled")
        return False
    try:
        app = SleepReport(
            since=since,
            until=until,
            fname=fname,
            fmt=fmt,
            tool_debug=tool_debug,
            report_debug=report_debug,
        )
    except sqlite3.OperationalError as e:
        print(f"Failed to generate report: {e}")
        return False
    except PermissionError as e:
        print(f"Failed to generate report: {e}")
        return False
    try:
        app.run()
    except PermissionError as e:
        print(f"Failed to generate report: {e}")
        return False
    except ValueError as e:
        print(f"Failed to generate report: {e}")
        return False
    display_report_file(fname, fmt)
    return True


def run_test_cycle(
    duration, wait, count, fmt, fname, force, debug, rand, logind, bios_debug
) -> bool:
    """Run a test"""
    app = Installer(tool_debug=debug)
    app.set_requirements("iasl", "ethtool", "edid-decode")
    if not app.install_dependencies():
        print("Failed to install dependencies")
        return False

    try:
        duration, wait, count = prompt_test_arguments(duration, wait, count, rand)
        total_seconds = (duration + wait) * count
        until_time = datetime.now() + timedelta(seconds=total_seconds)
        since, until, fname, fmt, report_debug = prompt_report_arguments(
            datetime.now().isoformat(), until_time.isoformat(), fname, fmt, True
        )
    except KeyboardInterrupt:
        sys.exit("\nTest cancelled")

    if not confirm_overwrite_report(fname):
        sys.exit("Test cancelled")

    try:
        app = PrerequisiteValidator(debug)
        run = app.run()
    except PermissionError as e:
        print(f"Failed to run prerequisite check: {e}")
        return False
    app.report()

    if run or force:
        app = SleepValidator(tool_debug=debug, bios_debug=bios_debug)

        run = app.run(
            duration=duration,
            wait=wait,
            count=count,
            rand=rand,
            logind=logind,
        )
        until = datetime.now()
    else:
        since = None
        until = None

    app = SleepReport(
        since=since,
        until=until,
        fname=fname,
        fmt=fmt,
        tool_debug=debug,
        report_debug=report_debug,
    )
    app.run()

    # open report in browser if it's html
    display_report_file(fname, fmt)

    return True


def install(debug) -> None:
    """Install the tool"""
    installer = Installer(tool_debug=debug)
    installer.set_requirements("iasl", "ethtool", "edid-decode")
    if not installer.install_dependencies():
        sys.exit("Failed to install dependencies")
    try:
        app = PrerequisiteValidator(debug)
        run = app.run()
    except PermissionError as e:
        sys.exit(f"Failed to run prerequisite check: {e}")
    if not run:
        app.report()
        sys.exit("Failed to meet prerequisites")

    if not installer.install():
        sys.exit("Failed to install")


def uninstall(debug) -> None:
    """Uninstall the tool"""
    app = Installer(tool_debug=debug)
    if not app.remove():
        sys.exit("Failed to remove")


def parse_args():
    """Parse command line arguments"""
    parser = argparse.ArgumentParser(
        description="Swiss army knife for analyzing Linux s2idle problems",
        epilog="The tool can run an immediate test with the 'test' command "
        "or can be used to hook into systemd for building reports later.\n"
        "All optional arguments will be prompted if needed.\n"
        "To use non-interactively, please populate all optional arguments.",
    )
    subparsers = parser.add_subparsers(help="Possible commands", dest="action")

    # 'test' command
    test_cmd = subparsers.add_parser("test", help="Run amd-s2idle test and report")
    test_cmd.add_argument("--count", help=Headers.CountDescription)
    test_cmd.add_argument(
        "--duration",
        help=Headers.DurationDescription,
    )
    test_cmd.add_argument(
        "--wait",
        help=Headers.WaitDescription,
    )
    test_cmd.add_argument(
        "--logind", action="store_true", help="Use logind to suspend system"
    )
    test_cmd.add_argument(
        "--random",
        action="store_true",
        help="Run sleep cycles for random durations and wait, using the "
        "--duration and --wait arguments as an upper bound",
    )
    test_cmd.add_argument(
        "--force",
        action="store_true",
        help="Run suspend test even if prerequisites failed",
    )
    test_cmd.add_argument(
        "--format",
        choices=Defaults.format_choices,
        help="Report format",
    )
    test_cmd.add_argument(
        "--tool-debug",
        action="store_true",
        help="Enable tool debug logging",
    )
    test_cmd.add_argument(
        "--bios-debug",
        action="store_true",
        help="Enable BIOS debug logging instead of notify logging",
    )
    test_cmd.add_argument("--report-file", help=Headers.ReportFileDescription)

    # 'report' command
    report_cmd = subparsers.add_parser(
        "report", help="Generate amd-s2idle report from previous runs"
    )
    report_cmd.add_argument(
        "--since",
        help=Headers.SinceDescription,
    )
    report_cmd.add_argument(
        "--until",
        default=Defaults.until.isoformat(),
        help=Headers.UntilDescription,
    )
    report_cmd.add_argument("--report-file", help=Headers.ReportFileDescription)
    report_cmd.add_argument(
        "--format",
        choices=Defaults.format_choices,
        help="Report format",
    )
    report_cmd.add_argument(
        "--tool-debug",
        action="store_true",
        help="Enable tool debug logging",
    )
    report_cmd.add_argument(
        "--report-debug",
        action=argparse.BooleanOptionalAction,
        help="Include debug messages in report (WARNING: can significantly increase report size)",
    )

    # if running in a venv, install/uninstall hook options
    if sys.prefix != sys.base_prefix:
        install_cmd = subparsers.add_parser(
            "install", help="Install systemd s2idle hook"
        )
        uninstall_cmd = subparsers.add_parser(
            "uninstall", help="Uninstall systemd s2idle hook"
        )
        install_cmd.add_argument(
            "--tool-debug",
            action="store_true",
            help="Enable tool debug logging",
        )
        uninstall_cmd.add_argument(
            "--tool-debug",
            action="store_true",
            help="Enable tool debug logging",
        )

    parser.add_argument(
        "--version", action="store_true", help="Show version information"
    )

    if len(sys.argv) == 1:
        parser.print_help(sys.stderr)
        sys.exit(1)

    return parser.parse_args()


def main() -> None | int:
    """Main function"""
    args = parse_args()
    ret = False
    if args.action == "install":
        relaunch_sudo()
        install(args.tool_debug)
    elif args.action == "uninstall":
        relaunch_sudo()
        uninstall(args.tool_debug)
    elif args.action == "report":
        ret = report(
            args.since,
            args.until,
            args.report_file,
            args.format,
            args.tool_debug,
            args.report_debug,
        )
    elif args.action == "test":
        relaunch_sudo()
        ret = run_test_cycle(
            args.duration,
            args.wait,
            args.count,
            args.format,
            args.report_file,
            args.force,
            args.tool_debug,
            args.random,
            args.logind,
            args.bios_debug,
        )
    elif args.version:
        print(version())
        return
    else:
        sys.exit("no action specified")
    show_log_info()
    if ret is False:
        return 1
    return
