#!/usr/bin/env python3
"""
EDMC.py - Command-line interface. Requires prior setup through the GUI.

Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
from __future__ import annotations

import argparse
import json
import locale
import os
import queue
import sys
from time import sleep, time
from typing import TYPE_CHECKING, Any

# isort: off
os.environ["EDMC_NO_UI"] = "1"

# See EDMCLogging.py docs.
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
from EDMCLogging import edmclogger, logger, logging

if TYPE_CHECKING:
    from logging import TRACE  # type: ignore # noqa: F401 # needed to make mypy happy

edmclogger.set_channels_loglevel(logging.INFO)

# isort: on
import collate
import commodity
import companion
import edshipyard
from l10n import translations as tr
import loadout
import outfitting
import shipyard
import stats
from commodity import COMMODITY_DEFAULT
from config import appcmdname, appversion, config
from monitor import monitor
from update import EDMCVersion, Updater, check_for_fdev_updates

sys.path.append(config.internal_plugin_dir)
# This import must be after the sys.path.append.
# The sys.path.append has to be after `import sys` and `from config import config`
# isort: off
import eddn  # noqa: E402


# isort: on


def log_locale(prefix: str) -> None:
    """Log the current state of locale settings."""
    logger.debug(f'''Locale: {prefix}
Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)}
Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)}
Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)}
Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)}
Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
                 )


tr.install_dummy()

SERVER_RETRY = 5  # retry pause for Companion servers [s]
EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR, EXIT_ARGS, \
    EXIT_JOURNAL_READ_ERR, EXIT_COMMANDER_UNKNOWN = range(9)


def versioncmp(versionstring) -> list:
    """Quick and dirty version comparison assuming "strict" numeric only version numbers."""
    return list(map(int, versionstring.split('.')))


def deep_get(target: dict | companion.CAPIData, *args: str, default=None) -> Any:
    """
    Walk into a dict and return the specified deep value.

    Example usage:

        >>> thing = {'a': {'b': {'c': 'foo'} } }
        >>> deep_get(thing, ('a', 'b', 'c'), None)
        'foo'
        >>> deep_get(thing, ('a', 'b'), None)
        {'c': 'foo'}
        >>> deep_get(thing, ('a', 'd'), None)
        None

    :param target: The dict to walk into for the desired value.
    :param args: The list of keys to walk down through.
    :param default: What to return if the target has no value.
    :return: The value at the target deep key.
    """
    if not hasattr(target, 'get'):
        raise ValueError(f"Cannot call get on {target} ({type(target)})")

    current = target
    for arg in args:
        res = current.get(arg)
        if res is None:
            return default
        current = res
    return current


def main():  # noqa: C901, CCR001
    """Run the main code of the program."""
    try:
        # arg parsing
        parser = argparse.ArgumentParser(
            prog=appcmdname,
            description='Prints the current system and station (if docked) to stdout and optionally writes player '
                        'status, ship locations, ship loadout and/or station data to file. '
                        'Requires prior setup through the accompanying GUI app.'
        )

        parser.add_argument('-v', '--version', help='print program version and exit', action='store_const', const=True)
        group_loglevel = parser.add_mutually_exclusive_group()
        group_loglevel.add_argument('--loglevel',
                                    metavar='loglevel',
                                    help='Set the logging loglevel to one of: '
                                         'CRITICAL, ERROR, WARNING, INFO, DEBUG, TRACE',
                                    )

        parser.add_argument(
            '--trace',
            help='Set the Debug logging loglevel to TRACE',
            action='store_true',
        )

        parser.add_argument(
            '--trace-on',
            help='Mark the selected trace logging as active. "*" or "all" is equivalent to --trace-all',
            action='append',
        )

        parser.add_argument(
            "--trace-all",
            help='Force trace level logging, with all possible --trace-on values active.',
            action='store_true'
        )

        parser.add_argument('-a', metavar='FILE', help='write ship loadout to FILE in Companion API json format')
        parser.add_argument('-e', metavar='FILE', help='write ship loadout to FILE in E:D Shipyard plain text format')
        parser.add_argument('-l', metavar='FILE', help='write ship locations to FILE in CSV format')
        parser.add_argument('-m', metavar='FILE', help='write station commodity market data to FILE in CSV format')
        parser.add_argument('-o', metavar='FILE', help='write station outfitting data to FILE in CSV format')
        parser.add_argument('-s', metavar='FILE', help='write station shipyard data to FILE in CSV format')
        parser.add_argument('-t', metavar='FILE', help='write player status to FILE in CSV format')
        parser.add_argument('-d', metavar='FILE', help='write raw JSON data to FILE')
        parser.add_argument('-n', action='store_true', help='send data to EDDN')
        parser.add_argument('-p', metavar='CMDR', help='Returns data from the specified player account')
        parser.add_argument('-j', help=argparse.SUPPRESS)  # Import JSON dump
        args = parser.parse_args()

        if args.version:
            updater = Updater()
            newversion: EDMCVersion | None = updater.check_appcast()
            if newversion:
                # LANG: Update Available Text
                newverstr: str = tr.tl("{NEWVER} is available").format(NEWVER=newversion.title)
                print(f'{appversion()} ({newverstr})')
            else:
                print(appversion())
            return

        level_to_set: int | None = None
        if args.trace or args.trace_on:
            level_to_set = logging.TRACE  # type: ignore # it exists
            logger.info('Setting TRACE level debugging due to either --trace or a --trace-on')

        if args.trace_all or (args.trace_on and ('*' in args.trace_on or 'all' in args.trace_on)):
            level_to_set = logging.TRACE_ALL  # type: ignore # it exists
            logger.info('Setting TRACE_ALL level debugging due to either --trace-all or a --trace-on *|all')

        if level_to_set is not None:
            logger.setLevel(level_to_set)
            edmclogger.set_channels_loglevel(level_to_set)

        elif args.loglevel:
            if args.loglevel not in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE'):
                print('loglevel must be one of: CRITICAL, ERROR, WARNING, INFO, DEBUG, TRACE', file=sys.stderr)
                sys.exit(EXIT_ARGS)
            edmclogger.set_channels_loglevel(args.loglevel)

        logger.debug(f'Startup v{appversion()} : Running on Python v{sys.version}')
        logger.debug(f'''Platform: {sys.platform}
                    argv[0]: {sys.argv[0]}
                    exec_prefix: {sys.exec_prefix}
                    executable: {sys.executable}
                    sys.path: {sys.path}'''
                     )
        if args.trace_on and len(args.trace_on) > 0:
            import config as conf_module

            conf_module.trace_on = [x.casefold() for x in args.trace_on]  # duplicate the list just in case
            for d in conf_module.trace_on:
                logger.info(f'marked {d} for TRACE')

        log_locale('Initial Locale')

        if args.j:
            logger.debug('Import and collate from JSON dump')
            # Import and collate from JSON dump
            #
            # Try twice, once with the system locale and once enforcing utf-8. If the file was made on the current
            # system, chances are its the current locale, and not utf-8. Otherwise if it was copied, its probably
            # utf8. Either way, try the system FIRST because reading something like cp1251 in UTF-8 results in garbage
            # but the reverse results in an exception.
            json_file = os.path.abspath(args.j)
            try:
                with open(json_file) as file_handle:
                    data = json.load(file_handle)
            except UnicodeDecodeError:
                with open(json_file, encoding='utf-8') as file_handle:
                    data = json.load(file_handle)
            config.set('querytime', int(os.path.getmtime(args.j)))

        else:
            # Get state from latest Journal file
            logger.debug('Getting state from latest journal file')
            try:
                monitor.currentdir = config.get_str('journaldir', default=config.default_journal_dir)
                if not monitor.currentdir:
                    monitor.currentdir = config.default_journal_dir

                logger.debug(f'logdir = "{monitor.currentdir}"')
                logfile = monitor.journal_newest_filename(monitor.currentdir)
                if logfile is None:
                    raise ValueError("None from monitor.journal_newest_filename")

                logger.debug(f'Using logfile "{logfile}"')
                with open(logfile, 'rb', 0) as loghandle:
                    for line in loghandle:
                        try:
                            monitor.parse_entry(line)

                        except Exception:
                            logger.debug(f'Invalid journal entry {line!r}')

            except Exception:
                logger.exception("Can't read Journal file")
                sys.exit(EXIT_JOURNAL_READ_ERR)

            if not monitor.cmdr:
                logger.error('Not available while E:D is at the main menu')
                sys.exit(EXIT_COMMANDER_UNKNOWN)

            # Get data from Companion API
            if args.p:
                logger.debug(f'Attempting to use commander "{args.p}"')
                cmdrs = config.get_list('cmdrs', default=[])
                if args.p in cmdrs:
                    idx = cmdrs.index(args.p)

                else:
                    for idx, cmdr in enumerate(cmdrs):
                        if cmdr.lower() == args.p.lower():
                            break
                    else:
                        raise companion.CredentialsError()
                companion.session.login(cmdrs[idx], monitor.is_beta)

            else:
                logger.debug(f'Attempting to use commander "{monitor.cmdr}" from Journal File')
                cmdrs = config.get_list('cmdrs', default=[])
                if monitor.cmdr not in cmdrs:
                    raise companion.CredentialsError()

                companion.session.login(monitor.cmdr, monitor.is_beta)

            ###################################################################
            # Initiate CAPI queries
            querytime = int(time())
            companion.session.station(query_time=querytime)

            # Wait for the response
            _capi_request_timeout = 60
            try:
                capi_response = companion.session.capi_response_queue.get(
                    block=True, timeout=_capi_request_timeout
                )

            except queue.Empty:
                logger.error(f'CAPI requests timed out after {_capi_request_timeout} seconds')
                sys.exit(EXIT_SERVER)

            ###################################################################

            # noinspection DuplicatedCode
            if isinstance(capi_response, companion.EDMCCAPIFailedRequest):
                logger.trace_if('capi.worker', f'Failed Request: {capi_response.message}')
                if capi_response.exception:
                    raise capi_response.exception
                raise ValueError(capi_response.message)

            logger.trace_if('capi.worker', 'Answer is not a Failure')
            if not isinstance(capi_response, companion.EDMCCAPIResponse):
                raise ValueError(f"Response was neither CAPIFailedRequest nor EDMCAPIResponse: {type(capi_response)}")

            data = capi_response.capi_data
            config.set('querytime', querytime)

        # Validation
        if not deep_get(data, 'commander', 'name', default='').strip():
            logger.error("No data['command']['name'] from CAPI")
            sys.exit(EXIT_SERVER)

        elif (
            not deep_get(data, 'lastSystem', 'name') or
            data['commander'].get('docked') and not deep_get(data, 'lastStarport', 'name')
        ):  # Only care if docked
            logger.error("No data['lastSystem']['name'] from CAPI")
            sys.exit(EXIT_SERVER)

        elif not deep_get(data, 'ship', 'modules') or not deep_get(data, 'ship', 'name', default=''):
            logger.error("No data['ship']['modules'] from CAPI")
            sys.exit(EXIT_SERVER)

        elif args.j:
            pass  # Skip further validation

        elif data['commander']['name'] != monitor.cmdr:
            raise companion.CmdrError()

        elif (
            data['lastSystem']['name'] != monitor.state['SystemName']
            or (
                (data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.state['StationName']
            )
            or data['ship']['id'] != monitor.state['ShipID']
            or data['ship']['name'].lower() != monitor.state['ShipType']
        ):
            raise companion.ServerLagging()

        # stuff we can do when not docked
        if args.d:
            logger.debug(f'Writing raw JSON data to "{args.d}"')
            out = json.dumps(dict(data), ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))
            with open(args.d, 'wb') as f:
                f.write(out.encode("utf-8"))

        if args.a:
            logger.debug(f'Writing Ship Loadout in Companion API JSON format to "{args.a}"')
            loadout.export(data, args.a)

        if args.e:
            logger.debug(f'Writing Ship Loadout in ED Shipyard plain text format to "{args.e}"')
            edshipyard.export(data, args.e)

        if args.l:
            logger.debug(f'Writing Ship Locations in CSV format to "{args.l}"')
            stats.export_ships(data, args.l)

        if args.t:
            logger.debug(f'Writing Player Status in CSV format to "{args.t}"')
            stats.export_status(data, args.t)

        if data['commander'].get('docked'):
            print(f'{deep_get(data, "lastSystem", "name", default="Unknown")},'
                  f'{deep_get(data, "lastStarport", "name", default="Unknown")}'
                  )

        else:
            print(deep_get(data, 'lastSystem', 'name', default='Unknown'))

        if args.m or args.o or args.s or args.n or args.j:
            if not data['commander'].get('docked'):
                logger.error("Can't use -m, -o, -s, -n or -j because you're not currently docked!")
                return

            if not deep_get(data, 'lastStarport', 'name'):
                logger.error("No data['lastStarport']['name'] from CAPI")
                sys.exit(EXIT_LAGGING)

            # Ignore possibly missing shipyard info
            if not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
                logger.error("No commodities or outfitting (modules) in CAPI data")
                return
        else:
            return

        # Finally - the data looks sane and we're docked at a station

        if args.j:
            logger.debug('Importing data from the CAPI return...')
            # Collate from JSON dump
            collate.addcommodities(data)
            collate.addmodules(data)
            collate.addships(data)

        if args.m:
            logger.debug(f'Writing Station Commodity Market Data in CSV format to "{args.m}"')
            if data['lastStarport'].get('commodities'):
                # Fixup anomalies in the commodity data
                fixed = companion.fixup(data)
                commodity.export(fixed, COMMODITY_DEFAULT, args.m)

            else:
                logger.error("Station doesn't have a market")

        if args.o:
            if data['lastStarport'].get('modules'):
                logger.debug(f'Writing Station Outfitting in CSV format to "{args.o}"')
                outfitting.export(data, args.o)

            else:
                logger.error("Station doesn't supply outfitting")

        if ((args.s or args.n) and not args.j and not data['lastStarport'].get('ships')
                and data['lastStarport']['services'].get('shipyard')):

            # Retry for shipyard
            sleep(SERVER_RETRY)
            companion.session.station(int(time()))
            # Wait for the response
            _capi_request_timeout = 60
            try:
                capi_response = companion.session.capi_response_queue.get(
                    block=True, timeout=_capi_request_timeout
                )

            except queue.Empty:
                logger.error(f'CAPI requests timed out after {_capi_request_timeout} seconds')
                sys.exit(EXIT_SERVER)

            if isinstance(capi_response, companion.EDMCCAPIFailedRequest):
                logger.error(f'Failed Request: {capi_response.message}')
                sys.exit(EXIT_SERVER)

            new_data = capi_response.capi_data
            # might have undocked while we were waiting for retry in which case station data is unreliable
            if (
                new_data['commander'].get('docked')
                and deep_get(new_data, 'lastSystem', 'name') == monitor.state['SystemName']
                and deep_get(new_data, 'lastStarport', 'name') == monitor.state['StationName']
            ):
                data = new_data

        if args.s:
            if deep_get(data, 'lastStarport', 'ships', 'shipyard_list'):
                logger.debug(f'Writing Station Shipyard in CSV format to "{args.s}"')
                shipyard.export(data, args.s)

            elif not args.j and monitor.stationservices and 'Shipyard' in monitor.stationservices:
                logger.error('Failed to get shipyard data')

            else:
                logger.error("Station doesn't have a shipyard")

        if args.n:
            try:
                eddn_sender = eddn.EDDN(None)
                logger.debug('Sending Market, Outfitting and Shipyard data to EDDN...')
                eddn_sender.export_commodities(data, monitor.is_beta)
                eddn_sender.export_outfitting(data, monitor.is_beta)
                eddn_sender.export_shipyard(data, monitor.is_beta)

            except Exception:
                logger.exception('Failed to send data to EDDN')

    except companion.ServerConnectionError:
        logger.exception('Exception while contacting server')
        sys.exit(EXIT_SERVER)

    except companion.ServerError:
        logger.exception('Frontier CAPI Server returned an error')
        sys.exit(EXIT_SERVER)

    except companion.CredentialsError:
        logger.error('Frontier CAPI Server: Invalid Credentials')
        sys.exit(EXIT_CREDENTIALS)

    # Companion API problem
    except companion.ServerLagging:
        logger.error(
            'Mismatch(es) between CAPI and Journal for at least one of: '
            'StarSystem, Last Star Port, Ship ID or Ship Name/Type'
        )
        sys.exit(EXIT_SERVER)

    except companion.CmdrError:  # Companion API return doesn't match Journal
        logger.error(
            f'Commander "{data["commander"]["name"]}" from CAPI doesn\'t match '
            f'"{monitor.cmdr}" from Journal'
        )
        sys.exit(EXIT_SERVER)

    except Exception:
        logger.exception('"other" exception')
        sys.exit(EXIT_SERVER)


if __name__ == '__main__':
    try:
        check_for_fdev_updates(silent=True)
        main()
    except KeyboardInterrupt:
        logger.info("Ctrl+C Detected, Attempting Clean Shutdown")
    logger.debug('Exiting')
    sys.exit(EXIT_SUCCESS)