import json
import argparse
from collections import namedtuple
from . import pretty
from . import __doc__ as doc
from . import __version__ as version


SEP_COMMON = ':'
SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
PRETTIFY_STDOUT_TTY_ONLY = object()


class ParseError(Exception):
    pass


KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep', 'orig'])


class KeyValueType(object):
    """A type used with `argparse`."""
    def __init__(self, *separators):
        self.separators = separators

    def __call__(self, string):
        found = dict((string.find(sep), sep)
                     for sep in self.separators
                     if string.find(sep) != -1)

        if not found:
            #noinspection PyExceptionInherit
            raise argparse.ArgumentTypeError(
                '"%s" is not a valid value' % string)
        sep = found[min(found.keys())]
        key, value = string.split(sep, 1)
        return KeyValue(key=key, value=value, sep=sep, orig=string)


def parse_items(items, data=None, headers=None):
    """Parse `KeyValueType` `items` into `data` and `headers`."""
    if headers is None:
        headers = {}
    if data is None:
        data = {}
    for item in items:
        value = item.value
        if item.sep == SEP_HEADERS:
            target = headers
        elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:
            if item.sep == SEP_DATA_RAW_JSON:
                try:
                    value = json.loads(item.value)
                except ValueError:
                    raise ParseError('%s is not valid JSON' % item.orig)
            target = data
        else:
            raise ParseError('%s is not valid item' % item.orig)

        if item.key in target:
            ParseError('duplicate item %s (%s)' % (item.key, item.orig))

        target[item.key] = value

    return headers, data


def _(text):
    """Normalize white space."""
    return ' '.join(text.strip().split())


parser = argparse.ArgumentParser(description=doc.strip(),)
parser.add_argument('--version', action='version', version=version)

# Content type.
#############################################

group_type = parser.add_mutually_exclusive_group(required=False)
group_type.add_argument(
    '--json', '-j', action='store_true',
    help=_('''
        Serialize data items as a JSON object and set
        Content-Type to application/json, if not specified.
    ''')
)
group_type.add_argument(
    '--form', '-f', action='store_true',
    help=_('''
        Serialize data items as form values and set
        Content-Type to application/x-www-form-urlencoded,
         if not specified.
     ''')
)


# Output options.
#############################################

parser.add_argument(
    '--traceback', action='store_true', default=False,
    help=_('''
        Print exception traceback should one occur.
    ''')
)
parser.add_argument(
    '--verbose', action='store_true', default=False,
    help=_('''
        Print HTTP request.
    ''')
)

prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument(
    '--pretty', '-p', dest='prettify', action='store_true',
    default=PRETTIFY_STDOUT_TTY_ONLY,
    help=_('''
        If stdout is a terminal,
        the response is prettified by default (colorized and
        indented if it is JSON). This flag ensures
        prettifying even when stdout is redirected.
    ''')
)
prettify.add_argument(
    '--ugly', '-u', dest='prettify', action='store_false',
    help=_('''
        Do not prettify the response.
    ''')
)

only = parser.add_mutually_exclusive_group(required=False)
only.add_argument(
    '--headers', '-t', dest='print_body',
    action='store_false', default=True,
    help=('''
        Print only the response headers.
    ''')
)
only.add_argument(
    '--body', '-b', dest='print_headers',
    action='store_false', default=True,
    help=('''
        Print only the response body.
    ''')
)
parser.add_argument(
    '--style', '-s', dest='style', default='solarized', metavar='STYLE',
    choices=pretty.AVAILABLE_STYLES,
    help=_('''
        Output coloring style, one of %s. Defaults to solarized.
    ''') % ', '.join(sorted(pretty.AVAILABLE_STYLES))
)

# ``requests.request`` keyword arguments.
parser.add_argument(
    '--auth', '-a', help='username:password',
    type=KeyValueType(SEP_COMMON)
)
parser.add_argument(
    '--verify',
    help=_('''
        Set to "yes" to check the host\'s SSL certificate.
        You can also pass the  path to a CA_BUNDLE
        file for private certs. You can also set
        the REQUESTS_CA_BUNDLE  environment variable.
    ''')
)
parser.add_argument(
    '--proxy', default=[], action='append',
    type=KeyValueType(SEP_COMMON),
    help=_('''
        String mapping protocol to the URL of the proxy
        (e.g. http:foo.bar:3128).
    ''')
)
parser.add_argument(
    '--base', metavar='BASE', type=str, default=None,
    help=_('''
        Base URL for the request.
    ''')
)
parser.add_argument(
    '--allow-redirects', default=False, action='store_true',
    help=_('''
        Set this flag if full redirects are allowed
        (e.g. re-POST-ing of data at new ``Location``)
    ''')
)
parser.add_argument(
    '--file', metavar='PATH', type=argparse.FileType(),
    default=[], action='append',
    help='File to multipart upload'
)
parser.add_argument(
    '--timeout', type=float,
    help=_('''
        Float describes the timeout of the request
        (Use socket.setdefaulttimeout() as fallback).
    ''')
)


# Positional arguments.
#############################################

parser.add_argument(
    'method', metavar='METHOD',
    help=_('''
        HTTP method to be used for the request
        (GET, POST, PUT, DELETE, PATCH, ...).
    ''')
)
parser.add_argument(
    'url', metavar='URL',
    help=_('''
        Protocol defaults to http:// if the
        URL does not include it.
    ''')
)
parser.add_argument(
    'items', nargs='*',
    type=KeyValueType(SEP_COMMON, SEP_DATA, SEP_DATA_RAW_JSON),
    help=_('''
        HTTP header (key:value), data field (key=value)
        or raw JSON field (field:=value).
    ''')
)
