import argparse
import importlib


parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd', metavar='{clone,run,test,logs,config}')

# TODO
# This is used to run a lightweight web server that does the following:
# - Vends Python library packages locally so they can be tested in the context of services
# - Exposes information about what services are running locally
# - Allows local config to resolve the correct ports automatically
# daemon = subparsers.add_parser('daemon')
# daemon.add_argument('--port', default='8016', help='override the default port')

run = subparsers.add_parser('run', description='Run a service or web client locally')
run_config = run.add_mutually_exclusive_group()
run_config.add_argument('--config-path', help='path to local configuration set')
run_config.add_argument('--config-id', help='configuration set ID')
run_config.add_argument('--config-namespace', default='local', help='use the latest configuration set from this namespace')
run.add_argument('--pkg-dir', default='.', help='local package directory (defaults to current directory)')
run.add_argument('--port', help='override the default port')
run.add_argument('--no-cache', action='store_true', help='rebuild the docker image without the cache')
run.add_argument(
  '--replace',
  action='store_true',
  help='replace anything running on the target port (by killing the associated process)'
)
run.add_argument(
  '--main',
  nargs=argparse.REMAINDER,
  help='run the package as a module (by invoking its __main__ file). Any successive arguments will be passed as-is. Note that this can only reference files with relative path names inside the current directory (the current directory is mounted a volume in the docker container).'
)
# run.add_argument(
#   '--soft',
#   action='store_true',
#   help='run the service directly using a lightweight server without a docker image - dependencies must be installed '
#        'locally'
# )
# run.add_argument(
#   '--install',
#   nargs='+',
#   help='[NOT YET SUPPORTED, USE --soft FOR NOW] a list of paths to python packages to build and install, overriding '
#        'any version specified in the requirements.txt file. This option is useful if you are developing a library '
#        'package and want to test it in the context of a service. Packages will be built / installed using: pip install '
#        '<pkg_dir>'
# )

TESTS_HELP = '''run the tests from the tst directory with optional restrictions

PATH: only run tests in the given relative file or directory (allows '*' wildcard matches)
CLASS_NAME: only run test classes with this name (allows '*' wildcard matches)
METHOD_NAME: only run test methods with this name (allows '*' wildcard matches)

Examples with a typical project setup:
Run all tests:
  $ op-dev test
  > tst/foo.py:TestA.test_1
  > tst/foo.py:TestA.test_2
  > tst/foo.py:TestB.test
  > tst/bar.py:TestC.test
  > tst/baz/biz.py:TestD.test
Run a specific test:
  $ op-dev test tst/foo.py:TestA.test_1
  > tst/foo.py:TestA.test_1
Run all tests in a file:
  $ op-dev test tst/foo.py
  > tst/foo.py:TestA.test_1
  > tst/foo.py:TestA.test_2
  > tst/foo.py:TestB.test
Run all tests in a file belonging to a certain class:
  $ op-dev test tst/foo.py:TestB
  > tst/foo.py:TestB.test
Run all tests in a file with a certain name belonging to any class:
  $ op-dev test tst/foo.py:*:test
  > tst/foo.py:TestB.test
Run all tests in any file with a certain name belonging to any class:
  $ op-dev test *:*:test
  > tst/foo.py:TestB.test
  > tst/bar.py:TestC.test
  > tst/baz/biz.py:TestD.test
Run multiple specific tests:
  $ op-dev test tst/foo.py:TestB.test tst/bar.py:TestC.test
  > tst/foo.py:TestB.test
  > tst/bar.py:TestC.test
'''

test = subparsers.add_parser('test', description='Test a service', formatter_class=argparse.RawTextHelpFormatter)
test.add_argument('--pkg-dir', default='.', help='local package directory\nDefaults to current directory')
test.add_argument('tests', nargs='*', metavar="PATH[:CLASS_NAME[.METHOD_NAME]]", help=TESTS_HELP)
test.add_argument('--url', required=True, help='URL of running service instance')

FILTER_REQUESTS_HELP = '''a single-argument python lambda function that should return True if the request message should be included in the output and False otherwise. Any exceptions are equivalent to returning False. The lambda is passed a complete request object as input which has the following properties:
- t: datetime
- si: the service instance ID
- r: route name
- ip: source IP address(es) - note that there can be multiple comma / whitespace delimited IPs as this field comes from the AWS X-Forwarded-For header
- ua: source user agent
- c: response code (as int)
- p: profile (unvalidated)
- pv: profile (validated)
- i: input (unvalidated)
- iv: input (validated)
- o: output

Example filters:
"lambda x: x['c'] != 200"
"lambda x: x['ip'].startswith('255.255.255.255')"
"lambda x: x['pv']['user_id'] == '00000000-0000-0000-0000-000000000000'"
"lambda x: x['si'] == 'abcd1234'"
'''

FORMAT_REQUESTS_HELP = '''format string using any of the properties from the complete request object (see --filter-requests). This option will also coalesce the distinct start and end request messages into a single record. Note that some of the available properties, such as 'ip' and 'ua' are not included in the default output.

Example format:
"%%(t)s %%(ip)s %%(ua)s"
'''

logs = subparsers.add_parser('logs', description='Pull logs for a service', formatter_class=argparse.RawTextHelpFormatter)
logs.add_argument('--url', required=True, help='URL of running service instance')
logs.add_argument('--from', dest='from_datetime', help='retrieve logs starting from this date / time (parsed using dateutil.parser)')
logs.add_argument('--to', dest='to_datetime', help='retrieve logs up to this date / time (parsed using dateutil.parser)')
logs.add_argument('--timezone', help="parse arguments and print timestamps in the specified timezone instead of the local one")
logs.add_argument('--levels', nargs='+', metavar='DEBUG|INFO|WARNING|ERROR|CRITICAL', help='if specified, only list messages that match the specified levels')
logs.add_argument('--name', nargs='+', metavar='NAME', help='if specified, only list messages produced by the specified named loggers')
logs.add_argument('--service-instance', nargs='+', metavar='SERVICE_INSTANCE_ID', help='if specified, only list messages produced by the specified service instance IDs')
logs.add_argument('--iso-format', action='store_true', help='print all timestamps in ISO format instead of the abridged versions inferred by the time range in question')
logs.add_argument('--filter-requests', metavar='FILTER', help=FILTER_REQUESTS_HELP)
logs.add_argument('--format-requests', metavar='FORMAT', help=FORMAT_REQUESTS_HELP)

config = subparsers.add_parser('config', description="View / update this utility's configutation")
config.add_argument('--secret-id', help='set the secret ID')
config.add_argument('--secret', help='set the secret')
config.add_argument('--op-dev-api-url', help=argparse.SUPPRESS)

clone = subparsers.add_parser('clone', description='Clone a package and manage the credentials automatically')
clone.add_argument('--pkg', required=True, help='package name to clone (also name of target directory)')
clone.add_argument('--team', help='team name')

# This is used to automatically authenticate our git requests
credential_helper = subparsers.add_parser('credential-helper')
credential_helper.add_argument('--team-id', required=True)
credential_helper.add_argument('operation', action='store', type=str)

# This is used to pull in config files for things that might not be run through the context of this CLI, namely web
# clients at this point. See the 'vue-cli-plugin-op-dev-config' for an example.
config_helper = subparsers.add_parser('config-helper')
config_helper.add_argument('--config-id', required=True)

# This is used to verify that local SSL certificates are installed and configured correctly so that local services can
# be brought up using SSL. Note that it might prompt for sudo access.
cert_helper = subparsers.add_parser('cert-helper')

# TODO: consider adding an 'ssh' command so you can connect to running instances:
# op-dev ssh --url URL --instance-id INSTANCE_ID
# Note that this would likely require some additional infrastructure to register instance IDs...


def main():
  args = parser.parse_args()

  # Check for a missing 'cmd' here instead of setting 'subparsers.required = True', so we can display a more sensible
  # error message (we had to override metavar to hide some subcommands that we don't want to expose to the user)
  if not args.cmd:
    parser.error('the following arguments are required: cmd')

  module_name = args.cmd.replace('-', '_')
  module = importlib.import_module('.' + module_name, package='app')

  try:
    module.run(parser, args)
  except KeyboardInterrupt:
    # Handle ctrl + C gracefully without printing an error message
    pass


if __name__ == '__main__':
  main()


# def get_pkg(args):
#   pkgs = set()
#   if args.latest_from_url:
#     # TODO: look up this service in the op-dev manager
#     pkgs.add('latest_from_url')
#   if args.pkg_dir:
#     # TODO: figure out the pkg name by analyzing the git config in the pkg dir
#     pkgs.add('pkg_dir')
#   if args.pkg:
#     pkgs.add(args.pkg)

#   if not pkg:
#     parser.error('Could not determine package name')

#   if len(pkgs)
#     #connect to this URL and pull the latest deployment's commit / config IDs







"""
This utility is designed to help you build python service packages / libraries

op-dev run [--latest-from-url http://localhost:5000]
op-dev test --url http://localhost:5000
op-dev logs --url http://localhost:5000
"""


# TODO: add tab-based autocompletion
# https://argcomplete.readthedocs.io/en/latest/
