#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
  docker-app v0.1.2
  author: diego.acuna@mailbox.org

  docker-app allows to define external docker-compose dependencies
  on a docker-compose app. Also, detects (for now) ruby/rails environments
  so an user can write rails command in a container using a more concise syntax.
"""
from contextlib import contextmanager
from argparse import RawTextHelpFormatter
import os
import sys
import yaml
import logging
import argparse
import subprocess

def execute_cmd(cmd, options=None):
  params_list = [cmd] if options is None else [cmd] + options
  p = subprocess.Popen(params_list, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  output, error = p.communicate()
  return output, error, p.returncode

@contextmanager
def cd(newdir):
  prevdir = os.getcwd()
  os.chdir(os.path.expanduser(newdir))
  try:
    yield
  finally:
    os.chdir(prevdir)

def execute_in_containers(cmd_params, all_containers=True):
  if all_containers:
    if 'dependencies' in parsed:
        for service in parsed['dependencies']:
          dwd = pwd.replace(dc_name, service)
          with cd(dwd):
            output, error, rcode = execute_cmd('docker-compose', cmd_params)
          if rcode == 0:
            logging.info("ON SERVICE {0}:\n{1}\n{2}".format(service, output, error))
          else:
            logging.info(error)
            sys.exit(-1)
  # now we execute the command in the current dir
  return execute_cmd('docker-compose', cmd_params)

# cons values
CONF_FILE   = 'docker-app.yml'
UP_CMD      = ['up', '-d']
STOP_CMD    = ['stop']
RESTART_CMD = ['restart']

if __name__ == "__main__":
  logging.basicConfig(level=logging.INFO)
  # valid arguments for command line parsing
  parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, epilog="Available Actions:\n" + \
                      "- up: start the container with all their dependencies (in background mode, no need to specify --all)\n" + \
                      "- stop: stop the container (with all their dependencies if --all is specified)\n" + \
                      "- restart: restart the container (with all their dependencies if --all is specified)\n" + \
                      "- rails: if the software detects a rails app, execute the given rails command\n" + \
                      "- bash: start the bash console in the main container")
  parser.add_argument("action", help="Action to execute")
  parser.add_argument('command', nargs='?', default='console', help="Aditional command to execute (for example using the rails action)")
  parser.add_argument('-a', '--all', action='store_true', help='Apply the action to all containers (including dependencies)')
  parser.add_argument('-c', '--container', help='Container where the action is going to be executed (for actions like bash or rails). ' + \
                                                'If it is not specified, then the main container (from config file or directory name) is used.')

  # by default we want to show argparse help messages
  if len(sys.argv)==1:
    parser.print_help()
    sys.exit(1)
  # get cmd arguments
  args = parser.parse_args()

  pwd = os.getcwd()
  # this is the name of the actual service, if no main is specified in the config
  # file, then we assume that this is the main container in the docker-compose file
  dc_name = pwd.split("/")[-1]

  parsed = {}  # by default we have no properties
  # we look for a docker-app.yml file
  if os.path.isfile(CONF_FILE):
    with open(CONF_FILE) as f:
      content = f.read()
    try:
      parsed = yaml.load(content)
    except yaml.scanner.ScannerError as e:
      logging.error("ERROR: {0}".format(e).format(e))
  # if we have a main property, then that is the main container in the docker-compose file
  main_container = parsed['main'] if 'main' in parsed else dc_name
  # proccess the current action
  if args.action == 'up':
    output, error, rcode = execute_in_containers(UP_CMD)
    logging.info("ON SERVICE {0}:\n{1}\n{2}".format(dc_name, output, error))
  elif args.action == 'stop':
    output, error, rcode = execute_in_containers(STOP_CMD, all_containers=args.all)
    logging.info("ON SERVICE {0}:\n{1}\n{2}".format(dc_name, output, error))
  elif args.action == 'restart':
    output, error, rcode = execute_in_containers(RESTART_CMD, all_containers=args.all)
    logging.info("ON SERVICE {0}:\n{1}\n{2}".format(dc_name, output, error))
  elif args.action == 'bash':
    if args.container:
      main_container = args.container
    sys.exit(subprocess.call(['docker-compose', 'exec', main_container, 'bash']))
  elif args.action in ['bundle', 'rails']:
    # first we check for a Gemfile
    if os.path.isfile('Gemfile'):
      logging.debug("Found a bundler configuration!")
      if args.container:
        main_container = args.container
      base_cmd = ['docker-compose', 'exec', main_container]
      if args.action == 'bundle':
        params = base_cmd + [args.action] if args.command is None else base_cmd + [args.action, args.command]
        logging.info("EXECUTING: " + " ".join(params))
        sys.exit(subprocess.call(params))
      if args.action == 'rails':
        params = base_cmd + ['bundle', 'exec', args.action] if args.command is None else base_cmd + ['bundle', 'exec', args.action, args.command]
        logging.info("EXECUTING: " + " ".join(params))
        sys.exit(subprocess.call(params))
    else:
      logging.info("No Gemfile in the actual directory.")
  elif args.action == 'exec':
    # this is the general exec command, its only a wrapper to the original exec in docker-compose
    if not args.command:
      logging.error("ERROR: You need to specify a command to execute")
    else:
      if args.container:
        main_container = args.container
      params = ['docker-compose', 'exec', main_container, args.command]
      logging.info("EXECUTING: " + " ".join(params))
      sys.exit(subprocess.call(params))
  else:
    logging.error("Command not found!")

