from __future__ import absolute_import, division, print_function

import os
import re
import time
import warnings
from collections import OrderedDict
from six import string_types
from six.moves.urllib.request import urlretrieve
from six.moves.urllib.error import HTTPError, URLError
from six.moves.urllib.parse import urljoin
import pandas as pd
import numpy as np

from .json import HttpJsonClient
from .error import DrmsQueryError, DrmsExportError, DrmsOperationNotSupported
from .utils import _pd_to_numeric_coerce, _split_arg, _extract_series_name

__all__ = ['SeriesInfo', 'ExportRequest', 'Client']


class SeriesInfo(object):
    """
    DRMS series details.

    Use :func:`Client.info` to create an instance.

    Attributes
    ----------
    name : string
        Series name.
    primekeys : list of strings
        Series primekeys.
    keywords : pandas.DataFrame
        Details about series keywords.
    links : pandas.DataFrame
        Details about series links.
    segments : pandas.DataFrame
        Details about series segments.
    note : string
        Series description.
    dbindex : list of strings
        Series database index.
    retention : int
        Default retention time.
    unitsize : int
        Storage unit size.
    archive : int
        Series archive flag.
    tapegroup : int
        Tape group.
    """
    def __init__(self, d, name=None):
        self._d = d
        self.name = name
        self.retention = self._d.get('retention')
        self.unitsize = self._d.get('unitsize')
        self.archive = self._d.get('archive')
        self.tapegroup = self._d.get('tapegroup')
        self.note = self._d.get('note')
        self.primekeys = self._d.get('primekeys')
        self.dbindex = self._d.get('dbindex')
        self.keywords = self._parse_keywords(d['keywords'])
        self.links = self._parse_links(d['links'])
        self.segments = self._parse_segments(d['segments'])

    @staticmethod
    def _parse_keywords(d):
        keys = [
            'name', 'type', 'recscope', 'defval', 'units', 'note', 'linkinfo']
        res = []
        for di in d:
            resi = []
            for k in keys:
                resi.append(di.get(k))
            res.append(tuple(resi))
        if not res:
            res = None  # workaround for older pandas versions
        res = pd.DataFrame(res, columns=keys)
        res.index = res.pop('name')
        res['is_time'] = (res.type == 'time')
        res['is_integer'] = (res.type == 'short')
        res['is_integer'] |= (res.type == 'int')
        res['is_integer'] |= (res.type == 'longlong')
        res['is_real'] = (res.type == 'float')
        res['is_real'] |= (res.type == 'double')
        res['is_numeric'] = (res.is_integer | res.is_real)
        return res

    @staticmethod
    def _parse_links(d):
        keys = ['name', 'target', 'kind', 'note']
        res = []
        for di in d:
            resi = []
            for k in keys:
                resi.append(di.get(k))
            res.append(tuple(resi))
        if not res:
            res = None  # workaround for older pandas versions
        res = pd.DataFrame(res, columns=keys)
        res.index = res.pop('name')
        return res

    @staticmethod
    def _parse_segments(d):
        keys = ['name', 'type', 'units', 'protocol', 'dims', 'note']
        res = []
        for di in d:
            resi = []
            for k in keys:
                resi.append(di.get(k))
            res.append(tuple(resi))
        if not res:
            res = None  # workaround for older pandas versions
        res = pd.DataFrame(res, columns=keys)
        res.index = res.pop('name')
        return res

    def __repr__(self):
        if self.name is None:
            return '<SeriesInfo>'
        else:
            return '<SeriesInfo "%s">' % self.name


class ExportRequest(object):
    """
    Class for handling data export requests.

    Use :func:`Client.export` or :func:`Client.export_from_id` to
    create an instance.

    Attributes
    ----------
    id : string
        Request ID.
    status : int
        Export request status.
    urls : pandas.DataFrame
        URLs of all downloadable files.
    request_url : string
        URL of the export request.
    method : string
        Data export method.
    protocol : string
        Data export protocol.
    data : pandas.DataFrame
        Records and filenames of the export request.
    dir : string
        Common directory of the requested files on the server.
    tarfile : string
        Filename, if a TAR file was requested.
    keywords : string
        Filename of textfile containing record keywords.
    """
    _status_code_ok = 0
    _status_code_notfound = 6
    _status_codes_pending = [1, 2, _status_code_notfound]
    _status_codes_ok_or_pending = [_status_code_ok] + _status_codes_pending

    def __init__(self, d, client):
        self._client = client
        self._requestid = None
        self._status = None
        self._download_urls_cache = None
        self._update_status(d)

    @classmethod
    def _create_from_id(cls, requestid, client):
        d = client._json.exp_status(requestid)
        return cls(d, client)

    def __repr__(self):
        idstr = str(None) if self._requestid is None else (
            '"%s"' % self._requestid)
        return '<ExportRequest id=%s, status=%d>' % (idstr, self._status)

    @staticmethod
    def _parse_data(d):
        keys = ['record', 'filename']
        res = None if d is None else [
            (di.get(keys[0]), di.get(keys[1])) for di in d]
        if not res:
            res = None  # workaround for older pandas versions
        res = pd.DataFrame(res, columns=keys)
        return res

    def _update_status(self, d=None):
        if d is None and self._requestid is not None:
            d = self._client._json.exp_status(self._requestid)
        self._d = d
        self._d_time = time.time()
        self._status = int(self._d.get('status', self._status))
        self._requestid = self._d.get('requestid', self._requestid)
        if self._requestid is None:
            # Apparently 'reqid' is used instead of 'requestid' for certain
            # protocols like 'mpg'
            self._requestid = self._d.get('reqid')
        if self._requestid == '':
            # Use None if the requestid is empty (url_quick + as-is)
            self._requestid = None

    def _raise_on_error(self, notfound_ok=True):
        if self._status in self._status_codes_ok_or_pending:
            if self._status != self._status_code_notfound or notfound_ok:
                return  # request has not failed (yet)
        msg = self._d.get('error')
        if msg is None:
            msg = 'DRMS export request failed.'
        msg += ' [status=%d]' % self._status
        raise DrmsExportError(msg)

    def _generate_download_urls(self):
        """Generate download URLs for the current request."""
        res = self.data.copy()
        data_dir = self.dir

        # Clear first record name for movies, as it is not a DRMS record.
        if self.protocol in ['mpg', 'mp4']:
            if res.record[0].startswith('movie'):
                res.record[0] = None

        # tar exports provide only a single TAR file with full path
        if self.tarfile is not None:
            data_dir = None
            res = pd.DataFrame(
                [(None, self.tarfile)], columns=['record', 'filename'])

        # If data_dir is None, the filename column should contain the full
        # path of the file and we need to extract the basename part. If
        # data_dir contains a directory, the filename column should contain
        # only the basename and we need to join it with the directory.
        if data_dir is None:
            res.rename(columns={'filename': 'fpath'}, inplace=True)
            split_fpath = res.fpath.str.split('/')
            res['filename'] = [sfp[-1] for sfp in split_fpath]
        else:
            res['fpath'] = data_dir + '/' + res.filename

        if self.method.startswith('url'):
            baseurl = self._client._server.http_download_baseurl
        elif self.method.startswith('ftp'):
            baseurl = self._client._server.ftp_download_baseurl
        else:
            raise RuntimeError(
                'Download is not supported for export method "%s"' %
                self.method)

        # Generate download URLs.
        urls = []
        for fp in res.fpath:
            while fp.startswith('/'):
                fp = fp[1:]
            urls.append(urljoin(baseurl, fp))
        res['url'] = urls

        # Remove rows with missing files.
        res = res[res.filename != 'NoDataFile']

        del res['fpath']
        return res

    @staticmethod
    def _next_available_filename(fname):
        """Find next available filename, append a number if neccessary."""
        i = 1
        new_fname = fname
        while os.path.exists(new_fname):
            new_fname = '%s.%d' % (fname, i)
            i += 1
        return new_fname

    @property
    def id(self):
        """(string) Request ID."""
        return self._requestid

    @property
    def status(self):
        """(int) Export request status."""
        return self._status

    @property
    def method(self):
        """(string) Export method."""
        return self._d.get('method')

    @property