import io # for StringIO or BytesIO
import csv
import json
import geojson
import os.path

import requests

import poobrains.form

from poobrains import app, flash, g, redirect
from poobrains.analysis import data, util

"""
data sources for the analysis system

If you're wondering where the `owner` argument to *Fieldset's `__init__` comes
from – that's some magic all Form subclasses have by way of `Form`s metaclass
inheriting from `poobrains.helpers.ClassOrInstanceBound` – in this case,
`owner` is the  source `Dataset` subclass or object.
"""

def dialect_from_dict(d):

        class dialect(csv.Dialect):
            _name = "temporary dialect"
                
        for name in ['delimiter', 'doublequote', 'escapechar', 'lineterminator', 'quotechar', 'quoting', 'skipinitialspace']:
            setattr(dialect, name, d[name])

        return dialect

class NoEditorForm(poobrains.formapp.MultistateFieldset):

    def __init__(self, owner, parent, **kwargs):
        super().__init__(parent, **kwargs)

    def build(self):
        app.logger.error("Missing editor form. This is a bug!")
        self['message'] = poobrains.form.fields.Message(value="Missing editor form. This is a bug!")

class CustomFormDataset(data.EphemeralDataset):

    class Meta:
        abstract = True

    editor_fieldset = NoEditorForm

    #def __init__(self, editor, **kwargs):
    #    super().__init__(**kwargs)
    #    self.editor = editor

    #@classmethod
    #def editor_fieldset(self, editor):
    #    raise NotImplementedError(f"Subclass of CustomFormDataset must implement .editor_fieldset, but {self.__class__.__name__} does not.")


class FileFieldset(poobrains.formapp.MultistateFieldset):

    def __init__(self, owner, parent, **kwargs):

        """
        Kinda-sorta fakes BoundForm behavior, but cheaper.
        """

        super().__init__(parent, **kwargs)
        self.owner = owner

    def build(self):

        super().build()

        if not 'file_action' in self.session:
            self.session['file_action'] = None

        if self.state == 'start':

            self['file'] = poobrains.form.fields.File()

            self['consumer'] = poobrains.form.fields.Select(choices=(
                ('csv', "CSV"),
                ('geojson', "geojson"),
            ))

        elif self.state == 'csv':

            csv_options = self.session['csv_options']

            if csv_options is None:
                guessed_dialect = csv.Sniffer().sniff(self.session['raw'][:4095])
                csv_options = {}
                for name in ['delimiter', 'doublequote', 'escapechar', 'lineterminator', 'quotechar', 'quoting', 'skipinitialspace']:
                    csv_options[name] = getattr(guessed_dialect, name)

            dialect = dialect_from_dict(csv_options)

            try:
                reader = csv.reader(io.StringIO(self.session['raw']), dialect)
            except csv.Error as e:
                flash("Can't read file as CSV with supplied options.", 'error')
                if app.debug:
                    flash(f"Full error message: {e}", 'error')
            else:

                table = poobrains.rendering.Table(title='CSV preview')
                for idx, row in enumerate(reader):
                    if idx == 5:
                        break
                    table.append(*row)

                self['table'] = poobrains.form.fields.RenderableWrapper(value=table)
                self.approve = poobrains.form.Button('submit', label='Approve')

            escaped = {}
            for key, value in csv_options.items():
                if isinstance(value, str):
                    escaped[key] = value.encode('unicode_escape').decode('utf-8')
                else:
                    escaped[key] = value

            self['first_line_as_keys'] = poobrains.form.fields.Checkbox(label="Derive keys from first line")
            self['comma_float'] = poobrains.form.fields.Checkbox(label="Handle floating point numbers that use a comma (,) instead of a point (.)")

            # CSV dialect fields
            self['delimiter'] = poobrains.form.fields.Text(label='delimiter', value=escaped['delimiter'])
            self['doublequote'] = poobrains.form.fields.Checkbox(label='doublequote', value=escaped['doublequote'])
            self['escapechar'] = poobrains.form.fields.Text(label='escapechar', value=escaped['escapechar'])
            self['lineterminator'] = poobrains.form.fields.Text(label='lineterminator', value=escaped['lineterminator'])
            self['quotechar'] = poobrains.form.fields.Text(label='quotechar', value=escaped['quotechar'])
            self['quoting'] = poobrains.form.fields.Select(label='quoting', type=poobrains.form.types.INT, choices=[(csv.QUOTE_NONE, 'QUOTE_NONE'), (csv.QUOTE_MINIMAL, 'QUOTE_MINIMAL'), (csv.QUOTE_NONNUMERIC, 'QUOTE_NONNUMERIC'), (csv.QUOTE_ALL, 'QUOTE_ALL')], value=escaped['quoting'])
            self['skipinitialspace'] = poobrains.form.fields.Checkbox(label='skipinitialspace', value=escaped['skipinitialspace'])

            self.change_csv_options = poobrains.form.Button(type='submit', label='Change CSV options')

    def process(self, submit):

        if submit == 'change_csv_options':
            unescape = lambda x: bytes(x, 'utf-8').decode('unicode_escape')
            self.session['csv_options'] = {
                'delimiter': unescape(self['delimiter'].value),
                'doublequote': self['doublequote'].value,
                'escapechar': unescape(self['escapechar'].value),
                'lineterminator': unescape(self['lineterminator'].value),
                'quotechar': unescape(self['quotechar'].value),
                'quoting': self['quoting'].value,
                'skipinitialspace': self['skipinitialspace'].value,
            }

            flash("Changed CSV dialect options.")

        else: # means parent form wasn't submitting via cancel button – i.e. the "load" button was clicked

            consumer = self['consumer'].value
            raw = self['file'].value.read()

            try:
                raw = raw.decode('utf-8')
            except UnicodeError as e:
                raw = raw.decode('utf-8', 'replace')

            self.session['consumer'] = consumer
            self.session['raw'] = raw

            if consumer == 'geojson':

                features = geojson.loads(raw)
                #ds = poobrains.analysis.EphemeralDataset()
                ds = self.owner()

                if features.get("type") == 'FeatureCollection':
                    col_name = features.get("id", 'featurecollection')
                    ds[col_name] = {
                        'title': col_name,
                        'description': '',
                        'dtype': 'geometry',
                        'color': None,
                        'observations': {}
                    }
                    for idx, feature in enumerate(features.features):
                        ds[col_name]['observations'][idx] = util.geojson_to_shapely(feature)

                self.parent.session['other'] = ds.to_dict(whole=True)
                self.parent.state = 'merge'

            elif consumer == 'csv':
                self.state = 'csv'
                self.session['data_action_info']['csv_options'] = None

            else:
                flash("Unhandled", 'error')



class FileDataset(CustomFormDataset):

    editor_fieldset = FileFieldset
    #@classmethod
    #def editor_fieldset(self, formapp, **kwargs):
    #    return FileFieldset(editor)


class RestFieldset(poobrains.formapp.MultistateFieldset):

    """
    Fieldset to use RestAPIs from the DataEditor.
    """

    def __init__(self, owner, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.owner = owner

    def build(self):

        readable_apis = RestAPIDefinition.list('read', g.user)
        api_choices = []
        for apidef in readable_apis:
            api_choices.append((apidef.handle_string, apidef.title))
        
        self['apidef'] = poobrains.form.fields.Select(label="API", choices=api_choices, readonly=True)

        if self.state == 'start':
            self['apidef'].readonly = False
            self.choose_api = poobrains.form.Button('submit', label='Choose')

        if self.state == 'request_params':
            self['apidef'].value = self.session['apidef']

            apidef = RestAPIDefinition.load(self.session['apidef'])
            self['parameters'] = self.summon_parameter_fieldset(apidef)

            self.send_request = poobrains.form.Button('submit', label='Send request')

    def summon_parameter_fieldset(self, apidef):

        subform = poobrains.form.Fieldset()

        for param_name, param_info in apidef.request.items():

            if not param_info['static']:
                param_dtype = util.__dtypes__[param_info['data_type']]
                subform[param_name] = param_dtype.form_field(label=param_info['title'], type=param_dtype.form_type, default=param_info['value'])

        return subform

    def process(self, submit):

        if submit == 'choose_api':
            self.session['apidef'] = self['apidef'].value
            self.state = 'request_params'

        elif submit == 'send_request':

            apidef = RestAPIDefinition.load(self.session['apidef'])

            params_path = {}
            params_query = {}
            params_post = {}

            for param_name, param_info in apidef.request.items():

                if param_info['type'] == 'path':
                    key = int(param_name)
                    if param_info['static']:
                        params_path[key] = param_info['value']
                    else:
                        params_path[key] = self['parameters'][param_name].value or param_info['value']
    
                elif param_info['type'] == 'query':
                    if param_info['static']:
                        params_query[param_name] = param_info['value']
                    else:
                        params_query[param_name] = self['parameters'][param_name].value or param_info['value']

                elif param_info['type'] == 'post':
                    if param_info['static']:
                        params_post[param_name] = param_info['value']
                    else:
                        params_post[param_name] = self['parameters'][param_name].value or param_info['value']

            method = 'POST' if len(params_post) else 'GET'
            url = os.path.join(*[apidef.base_url] + [str(params_path[k]) for k in sorted(params_path.keys())])
            response = requests.request(method, url, params=params_query, data=params_post)

            param_keys = {}

            for param_name, param_info in apidef.response.items():

                #before_idx, after_idx = param_info['address'].split('<n>')
                keys_before_index = []
                key_index = None
                keys_after_index = []
                address_components = param_info['address'].split('.')
                n_index = None
                for idx, address_component in enumerate(address_components):
                    if '<n>' in address_component:
                        n_index = idx
                        key_index = address_component

                    elif n_index is None:
                        keys_before_index.append(address_component)

                    else:
                        keys_after_index.append(address_component)

                param_keys[param_name] = {
                    'before_index': keys_before_index,
                    'index': key_index,
                    'after_index': keys_after_index
                }

            try:
                data = json.loads(response.text)
            except json.JSONDecodeError as e:
                flash("OMG TEH FAIL", 'error')
            else:

                ds = self.owner()
                for param_name, keys in param_keys.items():

                    param_info = apidef.response[param_name]
                    param_dtype = util.__dtypes__[param_info['data_type']]

                    ds[param_name] = {
                        'title': param_info['title'],
                        'description': param_info['description'],
                        'dtype': param_info['data_type'],
                        'color': None,
                        'observations': {}
                    }

                    parameter_data = data
                    for key in keys['before_index']:
                        parameter_data = parameter_data[key]

                    #if isinstance(parameter_data, dict):
                    #    for key, value in parameter_data.items():
                    #        app.debugger.set_trace() # FIXME: uuuh, what is this for, again?

                    if isinstance(parameter_data, list):
                        for idx, item_data in enumerate(parameter_data):
                            for key in keys['after_index']:
                                item_data = item_data[key]

                            ds[param_name]['observations'][idx] = param_dtype.string_numpy(item_data)

                    else:
                        flash(f"Invalid data type in REST response data: {str(type(parameter_data))}, expected list or dict.", 'error')

                self.parent.session['other'] = ds.to_dict(whole=True)
                self.parent.state = 'merge'

class RequestParameterFieldset(poobrains.formapp.MultistateFieldset):

    def __init__(self, parent, param_name=None, **kwargs):

        super().__init__(parent, **kwargs)
        self.param_name = param_name

        if param_name:
            self.param_info = self.formapp.session['request'][param_name]
        else:
            self.param_info = None

    def build(self):

            self['name'] = poobrains.form.fields.Text(label='Name')
            self['title'] = poobrains.form.fields.Text(label='Title')
            self['help_text'] = poobrains.form.fields.Text(label='Help text')

            self['type'] = poobrains.form.fields.Select(label='Parameter type', choices=self.formapp.parameter_type_choices)
            self['static'] = poobrains.form.fields.Checkbox(label='Static')
            self['data_type'] = poobrains.form.fields.Select(label='Data type', choices=self.formapp.data_type_choices)
            self['value'] = poobrains.form.fields.Text(label='Value', help_text="Always used for static parameters, only used as default for non-static parameters.")

            if self.param_info:

                self.title = f"Edit parameter '{self.param_name}'"

                self['name'].value = self.param_name
                self['name'].readonly = True

                self['title'].value = self.param_info['title']
                self['help_text'].value = self.param_info['help_text']
                self['static'].value = self.param_info['static']
                self['type'].value = self.param_info['type']
                self['data_type'].value = self.param_info['data_type']
                self['value'].value = self.param_info['value']

                self.delete = poobrains.form.Button('submit', label='Delete')
                self.apply = poobrains.form.Button('submit', label='Apply')

            else:
                self.title = 'Add parameter'
                self.add = poobrains.form.Button('submit', label='Add')

    def process(self, submit):

        name = self['name'].value

        if submit in ('add', 'apply'):
            self.formapp.session['request'][name] = {
                'title': self['title'].value,
                'help_text': self['help_text'].value,
                'type': self['type'].value,
                'static': self['static'].value,
                'data_type': self['data_type'].value,
                'value': self['value'].value
            }

        elif submit == 'delete':
            del(self.formapp.session['request'][name])
            flash(f"Deleted parameter {name}.")

        return redirect('#_')


class RequestFieldset(poobrains.formapp.MultistateFieldset):

    title = 'Parameters'

    def build(self):

        for param_name in self.formapp.session['request']:
            self[param_name] = RequestParameterFieldset(self, param_name)

        self['param_new'] = RequestParameterFieldset(self)
        if len(self.formapp.session['request']):
            self.apply = poobrains.form.Button('submit', label='Apply all')

    def process(self, submit):

        if submit == 'apply':
            for fieldset in self.fieldsets:
                if fieldset.name != 'param_new':
                    fieldset.process(self.child_submit(submit))
        else:
            return self.process_submitting_fieldset(submit)

        return redirect('#_')


class ResponseParameterFieldset(poobrains.formapp.MultistateFieldset):

    def __init__(self, parent, param_name=None, **kwargs):

        super().__init__(parent, **kwargs)
        self.param_name = param_name

        if param_name:
            self.param_info = self.formapp.session['response'][param_name]
        else:
            self.param_info = None

    def build(self):

        self['name'] = poobrains.form.fields.Text(label='Name')
        self['title'] = poobrains.form.fields.Text(label='Title')
        self['address'] = poobrains.form.fields.Text(label='Address', help_text='Address in the response dictionary, using a dot (.) as separator')
        self['data_type'] = poobrains.form.fields.Select(label='Data type', choices=self.formapp.data_type_choices)
        self['description'] = poobrains.form.fields.TextArea(label='Description')

        if self.param_info:

            self.title = f"Edit parameter '{self.param_name}'"

            self['name'].value = self.param_name
            self['name'].readonly = True

            self['title'].value = self.param_info['title']
            self['address'].value = self.param_info['address']
            self['data_type'].value = self.param_info['data_type']
            self['description'].value = self.param_info['description']

            self.delete = poobrains.form.Button('submit', label='Delete')
            self.apply = poobrains.form.Button('submit', label='Apply')

        else:
            self.title = 'Add parameter'
            self.add = poobrains.form.Button('submit', label='Add')

    def process(self, submit):

        name = self['name'].value
        if submit in ('add', 'apply'):
            self.formapp.session['response'][name] = {
                'title': self['title'].value,
                'address': self['address'].value,
                'data_type': self['data_type'].value,
                'description': self['description'].value
            }

        elif submit == 'delete':
            del(self.formapp.session['response'][name])
            flash(f"Deleted parameter {name}.")

        return redirect('#_')


class ResponseFieldset(poobrains.formapp.MultistateFieldset):

    title = 'Response'

    def build(self):

        for param_name in self.formapp.session['response']:
            self[param_name] = ResponseParameterFieldset(self, param_name)
        self['param_new'] = ResponseParameterFieldset(self)

    def process(self, submit):

        if submit == 'apply':
            for fieldset in self.fieldsets:
                if fieldset.name != 'param_new':
                    fieldset.process(self.child_submit(submit))
        else:
            subform = submit.split('.', 1)[1]
            return self.process_submitting_fieldset(submit)

        return redirect('#_')


class RestAPIEditor(poobrains.formapp.BoundFormApp):

    def __init__(self, parent, **kwargs):

        super().__init__(parent, **kwargs)

        if self.session_is_new:
            self.session['request'] = self.instance.request or {}
            self.session['response'] = self.instance.response or {}

        self['title'] = poobrains.form.fields.Text(label='Title', default=self.instance.title)
        self['base_url'] = poobrains.form.fields.Text(label='Base URL', default=self.instance.base_url)


        self['request'] = RequestFieldset(self)
        self['response'] = ResponseFieldset(self)

        self.data_type_choices = []
        for type in util.__dtypes__.keys():
            self.data_type_choices.append((type, type))

        self.parameter_type_choices = [
            ('path', 'URL Path'),
            ('query', 'URL query'),
            ('post', 'POST'),
        ]

        self.save = poobrains.form.Button('submit', label='Save')

    def process(self, submit):

        if submit == 'save':

            self.instance.request = self.session['request']
            self.instance.response = self.session['response']
            self.instance.lock = None

            self.instance.save()
            self.session_delete()
            flash('Saved API definition.')

        else:
            return self.process_submitting_fieldset(submit)

        return redirect('#_')


class RestAPI(CustomFormDataset):

    editor_fieldset = RestFieldset


class RestAPIDefinition(poobrains.auth.Administerable):

    title = poobrains.storage.fields.CharField()
    base_url = poobrains.storage.fields.CharField()
    request = poobrains.storage.fields.BSONField(null=True)
    response = poobrains.storage.fields.BSONField(null=True)
    
    form_edit = RestAPIEditor
