#! /usr/bin/env python
"""
A Command Line Wrapper to allow easy use of pyicloud for
command line scripts, and related.
"""

import argparse
import logging
import os
import pickle
import sys
from pprint import pformat
from typing import Any, Optional

from click import confirm

from pyicloud import PyiCloudService, utils
from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudServiceUnavailable
from pyicloud.services.findmyiphone import AppleDevice
from pyicloud.ssl_context import configurable_ssl_verification

DEVICE_ERROR = "Please use the --device switch to indicate which device to use."


def create_pickled_data(idevice: AppleDevice, filename: str) -> None:
    """
    This helper will output the idevice to a pickled file named
    after the passed filename.

    This allows the data to be used without resorting to screen / pipe
    scrapping.
    """
    with open(filename, "wb") as pickle_file:
        pickle.dump(
            idevice.data,
            pickle_file,
            protocol=pickle.HIGHEST_PROTOCOL,
        )


def _create_parser() -> argparse.ArgumentParser:
    """Create the parser."""
    parser = argparse.ArgumentParser(description="Find My iPhone CommandLine Tool")

    parser.add_argument(
        "--username",
        action="store",
        dest="username",
        default="",
        help="Apple ID to Use",
    )

    parser.add_argument(
        "--password",
        action="store",
        dest="password",
        default="",
        help=(
            "Apple ID Password to Use; if unspecified, password will be "
            "fetched from the system keyring."
        ),
    )

    parser.add_argument(
        "--china-mainland",
        action="store_true",
        dest="china_mainland",
        default=False,
        help="If the country/region setting of the Apple ID is China mainland",
    )

    parser.add_argument(
        "-n",
        "--non-interactive",
        action="store_false",
        dest="interactive",
        default=True,
        help="Disable interactive prompts.",
    )

    parser.add_argument(
        "--delete-from-keyring",
        action="store_true",
        dest="delete_from_keyring",
        default=False,
        help="Delete stored password in system keyring for this username.",
    )

    # Group for listing options
    list_group = parser.add_argument_group(
        title="Listing Options",
        description="Options for listing devices",
    )

    # Mutually exclusive group for listing options
    list_type_group = list_group.add_mutually_exclusive_group()
    list_type_group.add_argument(
        "--list",
        action="store_true",
        dest="list",
        default=False,
        help="Short Listings for Device(s) associated with account",
    )

    list_type_group.add_argument(
        "--llist",
        action="store_true",
        dest="longlist",
        default=False,
        help="Detailed Listings for Device(s) associated with account",
    )

    list_group.add_argument(
        "--locate",
        action="store_true",
        dest="locate",
        default=False,
        help="Retrieve Location for the iDevice (non-exclusive).",
    )

    # Restrict actions to a specific devices UID / DID
    parser.add_argument(
        "--device",
        action="store",
        dest="device_id",
        default=False,
        help="Only effect this device",
    )

    # Trigger Sound Alert
    parser.add_argument(
        "--sound",
        action="store_true",
        dest="sound",
        default=False,
        help="Play a sound on the device",
    )

    # Trigger Message w/Sound Alert
    parser.add_argument(
        "--message",
        action="store",
        dest="message",
        default=False,
        help="Optional Text Message to display with a sound",
    )

    # Trigger Message (without Sound) Alert
    parser.add_argument(
        "--silentmessage",
        action="store",
        dest="silentmessage",
        default=False,
        help="Optional Text Message to display with no sounds",
    )

    # Lost Mode
    parser.add_argument(
        "--lostmode",
        action="store_true",
        dest="lostmode",
        default=False,
        help="Enable Lost mode for the device",
    )

    parser.add_argument(
        "--lostphone",
        action="store",
        dest="lost_phone",
        default=False,
        help="Phone Number allowed to call when lost mode is enabled",
    )

    parser.add_argument(
        "--lostpassword",
        action="store",
        dest="lost_password",
        default=False,
        help="Forcibly active this passcode on the idevice",
    )

    parser.add_argument(
        "--lostmessage",
        action="store",
        dest="lost_message",
        default="",
        help="Forcibly display this message when activating lost mode.",
    )

    # Output device data to an pickle file
    parser.add_argument(
        "--outputfile",
        action="store_true",
        dest="output_to_file",
        default="",
        help="Save device data to a file in the current directory.",
    )

    parser.add_argument(
        "--log-level",
        action="store",
        dest="loglevel",
        choices=["error", "warning", "info", "none"],
        default="info",
        help="Set the logging level",
    )

    parser.add_argument(
        "--debug",
        action="store_true",
        help="Enable debug logging",
    )

    parser.add_argument(
        "--accept-terms",
        action="store_true",
        default=False,
        help="Automatically accept terms and conditions",
    )

    parser.add_argument(
        "--with-family",
        action="store_true",
        default=False,
        help="Include family devices",
    )

    parser.add_argument(
        "--session-dir",
        type=str,
        help="Directory to store session files",
    )

    parser.add_argument(
        "--http-proxy",
        type=str,
        help="Use HTTP proxy for requests",
    )

    parser.add_argument(
        "--https-proxy",
        type=str,
        help="Use HTTPS proxy for requests",
    )

    parser.add_argument(
        "--no-verify-ssl",
        action="store_true",
        default=False,
        help="Disable SSL certificate verification (WARNING: This makes the connection insecure)",
    )

    return parser


def _get_password(
    username: str,
    parser: argparse.ArgumentParser,
    command_line: argparse.Namespace,
) -> Optional[str]:
    """Which password we use is determined by your username, so we
    do need to check for this first and separately."""
    if not username:
        parser.error("No username supplied")

    password: Optional[str] = command_line.password
    if not password:
        password = utils.get_password(username, interactive=command_line.interactive)

    return password


def main() -> None:
    """Main commandline entrypoint."""
    parser: argparse.ArgumentParser = _create_parser()
    command_line: argparse.Namespace = parser.parse_args()
    level = logging.INFO

    if command_line.loglevel == "error":
        level = logging.ERROR
    elif command_line.loglevel == "warning":
        level = logging.WARNING
    elif command_line.loglevel == "info":
        level = logging.INFO
    elif command_line.loglevel == "none":
        level = None

    if command_line.debug:
        level = logging.DEBUG

    if level:
        logging.basicConfig(level=level)

    username: str = command_line.username.strip()
    china_mainland: bool = command_line.china_mainland

    if username and command_line.delete_from_keyring:
        utils.delete_password_in_keyring(username)

    with configurable_ssl_verification(
        verify_ssl=not command_line.no_verify_ssl,
        http_proxy=command_line.http_proxy or "",
        https_proxy=command_line.https_proxy or "",
    ):
        password: Optional[str] = _get_password(username, parser, command_line)

        api: Optional[PyiCloudService] = _authenticate(
            username,
            password,
            china_mainland,
            parser,
            command_line,
        )

        if not api:
            return
        _print_devices(api, command_line)


def _authenticate(
    username: str,
    password: Optional[str],
    china_mainland: bool,
    parser: argparse.ArgumentParser,
    command_line: argparse.Namespace,
) -> Optional[PyiCloudService]:
    api = None
    try:
        api = PyiCloudService(
            apple_id=username,
            password=password,
            china_mainland=china_mainland,
            cookie_directory=command_line.session_dir,
            accept_terms=command_line.accept_terms,
            with_family=command_line.with_family,
        )
        if (
            not utils.password_exists_in_keyring(username)
            and command_line.interactive
            and confirm("Save password in keyring?")
            and password
        ):
            utils.store_password_in_keyring(username, password)

        if api.requires_2fa:
            _handle_2fa(api)

        elif api.requires_2sa:
            _handle_2sa(api)
        return api
    except PyiCloudFailedLoginException as err:
        # If they have a stored password; we just used it and
        # it did not work; let's delete it if there is one.
        if not password:
            parser.error("No password supplied")

        if utils.password_exists_in_keyring(username):
            utils.delete_password_in_keyring(username)

        message: str = f"Bad username or password for {username}"

        print(err, file=sys.stderr)

        raise RuntimeError(message) from err


def _print_devices(api: PyiCloudService, command_line: argparse.Namespace) -> None:
    try:
        print(f"Number of devices: {len(api.devices)}", flush=True)
        for dev in api.devices:
            if not command_line.device_id or (
                command_line.device_id.strip().lower() == dev.id.strip().lower()
            ):
                # List device(s)
                _list_devices_option(command_line, dev)

                # Play a Sound on a device
                _play_device_sound_option(command_line, dev)

                # Display a Message on the device
                _display_device_message_option(command_line, dev)

                # Display a Silent Message on the device
                _display_device_silent_message_option(command_line, dev)

                # Enable Lost mode
                _enable_lost_mode_option(command_line, dev)
    except PyiCloudServiceUnavailable:
        print("iCloud - Find My service is unavailable.")


def _enable_lost_mode_option(
    command_line: argparse.Namespace, dev: AppleDevice
) -> None:
    if command_line.lostmode:
        if command_line.device_id:
            dev.lost_device(
                number=command_line.lost_phone.strip(),
                text=command_line.lost_message.strip(),
                newpasscode=command_line.lost_password.strip(),
            )
        else:
            raise RuntimeError(
                f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}"
            )


def _display_device_silent_message_option(
    command_line: argparse.Namespace, dev: AppleDevice
) -> None:
    if command_line.silentmessage:
        if command_line.device_id:
            dev.display_message(
                subject="A Silent Message",
                message=command_line.silentmessage,
                sounds=False,
            )
        else:
            raise RuntimeError(
                f"Silent Messages can only be played on a singular device. {DEVICE_ERROR}"
            )


def _display_device_message_option(
    command_line: argparse.Namespace, dev: AppleDevice
) -> None:
    if command_line.message:
        if command_line.device_id:
            dev.display_message(
                subject="A Message", message=command_line.message, sounds=True
            )
        else:
            raise RuntimeError(
                f"Messages can only be played on a singular device. {DEVICE_ERROR}"
            )


def _play_device_sound_option(
    command_line: argparse.Namespace, dev: AppleDevice
) -> None:
    if command_line.sound:
        if command_line.device_id:
            dev.play_sound()
        else:
            raise RuntimeError(
                f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n"
            )


def _list_devices_option(command_line: argparse.Namespace, dev: AppleDevice) -> None:
    location = dev.location if command_line.locate else None

    if command_line.output_to_file:
        create_pickled_data(
            dev,
            filename=(dev.name.strip().lower() + ".fmip_snapshot"),
        )

    if command_line.longlist:
        print("-" * 30)
        print(dev.name)
        for key in dev.data:
            print(
                f"{key:>30} - {pformat(dev.data[key]).replace(os.linesep, os.linesep + ' ' * 33)}"
            )
    elif command_line.list:
        print("-" * 30)
        print(f"Name           - {dev.name}")
        print(f"Display Name   - {dev.deviceDisplayName}")
        print(f"Location       - {location or dev.location}")
        print(f"Battery Level  - {dev.batteryLevel}")
        print(f"Battery Status - {dev.batteryStatus}")
        print(f"Device Class   - {dev.deviceClass}")
        print(f"Device Model   - {dev.deviceModel}")


def _handle_2fa(api: PyiCloudService) -> None:
    print("\nTwo-step authentication required.", "\nPlease enter validation code")

    code: str = input("(string) --> ")
    if not api.validate_2fa_code(code):
        print("Failed to verify verification code")
        sys.exit(1)

    print("")


def _handle_2sa(api: PyiCloudService) -> None:
    print("\nTwo-step authentication required.", "\nYour trusted devices are:")

    devices: list[dict[str, Any]] = _show_devices(api)

    print("\nWhich device would you like to use?")
    device_idx = int(input("(number) --> "))
    device: dict[str, Any] = devices[device_idx]
    if not api.send_verification_code(device):
        print("Failed to send verification code")
        sys.exit(1)

    print("\nPlease enter validation code")
    code: str = input("(string) --> ")
    if not api.validate_verification_code(device, code):
        print("Failed to verify verification code")
        sys.exit(1)

    print("")


def _show_devices(api: PyiCloudService) -> list[dict[str, Any]]:
    """Show devices."""
    devices: list[dict[str, Any]] = api.trusted_devices
    for i, device in enumerate(devices):
        phone_number: str = f"{device.get('deviceType')} to {device.get('phoneNumber')}"
        print(f"    {i}: {device.get('deviceName', phone_number)}")

    return devices


if __name__ == "__main__":
    main()
