import os

from stone.backend import CodeBackend
from stone.ir import (
    is_void_type,
    is_struct_type
)

from go_helpers import (
    HEADER,
    fmt_type,
    fmt_var,
    generate_doc,
)


class GoClientBackend(CodeBackend):
    def generate(self, api):
        for namespace in api.namespaces.values():
            if len(namespace.routes) > 0:
                self._generate_client(namespace)

    def _generate_client(self, namespace):
        file_name = os.path.join(self.target_folder_path, namespace.name,
                                 'client.go')
        with self.output_to_relative_path(file_name):
            self.emit_raw(HEADER)
            self.emit()
            self.emit('package %s' % namespace.name)
            self.emit()

            self.emit('// Client interface describes all routes in this namespace')
            with self.block('type Client interface'):
                for route in namespace.routes:
                    generate_doc(self, route)
                    self.emit(self._generate_route_signature(namespace, route))
            self.emit()

            self.emit('type apiImpl dropbox.Context')
            for route in namespace.routes:
                self._generate_route(namespace, route)
            self.emit('// New returns a Client implementation for this namespace')
            with self.block('func New(c dropbox.Config) Client'):
                self.emit('ctx := apiImpl(dropbox.NewContext(c))')
                self.emit('return &ctx')

    def _generate_route_signature(self, namespace, route):
        req = fmt_type(route.arg_data_type, namespace)
        res = fmt_type(route.result_data_type, namespace, use_interface=True)
        fn = fmt_var(route.name)
        if route.version != 1:
            fn += 'V%d' % route.version
        style = route.attrs.get('style', 'rpc')

        arg = '' if is_void_type(route.arg_data_type) else 'arg {req}'
        ret = '(err error)' if is_void_type(route.result_data_type) else \
            '(res {res}, err error)'
        signature = '{fn}(' + arg + ') ' + ret
        if style == 'download':
            signature = '{fn}(' + arg + \
                ') (res {res}, content io.ReadCloser, err error)'
        elif style == 'upload':
            signature = '{fn}(' + arg + ', content io.Reader) ' + ret
            if is_void_type(route.arg_data_type):
                signature = '{fn}(content io.Reader) ' + ret
        return signature.format(fn=fn, req=req, res=res)


    def _generate_route(self, namespace, route):
        out = self.emit
        fn = fmt_var(route.name)
        if route.version != 1:
            fn += 'V%d' % route.version
        err = fmt_type(route.error_data_type, namespace)
        out('//%sAPIError is an error-wrapper for the %s route' %
            (fn, route.name))
        with self.block('type {fn}APIError struct'.format(fn=fn)):
            out('dropbox.APIError')
            out('EndpointError {err} `json:"error"`'.format(err=err))
        out()

        signature = 'func (dbx *apiImpl) ' + self._generate_route_signature(
            namespace, route)
        with self.block(signature):
            if route.deprecated is not None:
                out('log.Printf("WARNING: API `%s` is deprecated")' % fn)
                if route.deprecated.by is not None:
                    out('log.Printf("Use API `%s` instead")' % fmt_var(route.deprecated.by.name))
                out()

            out('cli := dbx.Client')
            out()

            self._generate_request(namespace, route)
            self._generate_post()
            self._generate_response(route)
            ok_check = 'if resp.StatusCode == http.StatusOK'
            if fn == "Download":
                ok_check += ' || resp.StatusCode == http.StatusPartialContent'
            with self.block(ok_check):
                self._generate_result(route)
            self._generate_error_handling(namespace, route)

        out()

    def _generate_request(self, namespace, route):
        out = self.emit
        auth = route.attrs.get('auth', '')
        host = route.attrs.get('host', 'api')
        style = route.attrs.get('style', 'rpc')

        body = 'nil'
        if not is_void_type(route.arg_data_type):
            out('dbx.Config.LogDebug("arg: %v", arg)')

            out('b, err := json.Marshal(arg)')
            with self.block('if err != nil'):
                out('return')
            out()
            if host != 'content':
                body = 'bytes.NewReader(b)'
        if style == 'upload':
            body = 'content'

        headers = {}
        if not is_void_type(route.arg_data_type):
            if host == 'content' or style in ['upload', 'download']:
                headers["Dropbox-API-Arg"] = "string(b)"
            else:
                headers["Content-Type"] = '"application/json"'
        if style == 'upload':
            headers["Content-Type"] = '"application/octet-stream"'

        out('headers := map[string]string{')
        for k, v in sorted(headers.items()):
            out('\t"{}": {},'.format(k, v))
        out('}')
        if fmt_var(route.name) == "Download":
            out('for k, v := range arg.ExtraHeaders { headers[k] = v }')
        if auth != 'noauth' and auth != 'team':
            with self.block('if dbx.Config.AsMemberID != ""'):
                out('headers["Dropbox-API-Select-User"] = dbx.Config.AsMemberID')
        out()

        fn = route.name
        if route.version != 1:
            fn += '_v%d' % route.version
        authed = 'false' if auth == 'noauth' else 'true'
        out('req, err := (*dropbox.Context)(dbx).NewRequest("{}", "{}", {}, "{}", "{}", headers, {})'.format(
            host, style, authed, namespace.name, fn, body))
        with self.block('if err != nil'):
            out('return')

        out('dbx.Config.LogInfo("req: %v", req)')

        out()

    def _generate_post(self):
        out = self.emit

        out('resp, err := cli.Do(req)')

        with self.block('if err != nil'):
            out('return')
        out()

        out('dbx.Config.LogInfo("resp: %v", resp)')

    def _generate_response(self, route):
        out = self.emit
        style = route.attrs.get('style', 'rpc')
        if style == 'download':
            out('body := []byte(resp.Header.Get("Dropbox-API-Result"))')
            out('content = resp.Body')
        else:
            out('defer resp.Body.Close()')
            with self.block('body, err := ioutil.ReadAll(resp.Body);'
                            'if err != nil'):
                out('return')
            out()

        out('dbx.Config.LogDebug("body: %s", body)')

    def _generate_error_handling(self, namespace, route):
        out = self.emit
        style = route.attrs.get('style', 'rpc')
        with self.block('if resp.StatusCode == http.StatusConflict'):
            # If style was download, body was assigned to a header.
            # Need to re-read the response body to parse the error
            if style == 'download':
                out('defer resp.Body.Close()')
                with self.block('body, err = ioutil.ReadAll(resp.Body);'
                                'if err != nil'):
                    out('return')
            out('var apiError %sAPIError' % fmt_var(route.name))
            with self.block('err = json.Unmarshal(body, &apiError);'
                            'if err != nil'):
                out('return')
            out('err = apiError')
            out('return')
        auth_ns = "" if namespace.name == "auth" else "auth."
        with self.block('err = %sHandleCommonAuthErrors(dbx.Config, resp, body);'
                        'if err != nil' % auth_ns):
            out('return')
        out('err = dropbox.HandleCommonAPIErrors(dbx.Config, resp, body)')
        out('return')

    def _generate_result(self, route):
        out = self.emit
        if is_struct_type(route.result_data_type) and \
                route.result_data_type.has_enumerated_subtypes():
            out('var tmp %sUnion' % fmt_var(route.result_data_type.name, export=False))
            with self.block('err = json.Unmarshal(body, &tmp);'
                            'if err != nil'):
                out('return')
            with self.block('switch tmp.Tag'):
                for t in route.result_data_type.get_enumerated_subtypes():
                    with self.block('case "%s":' % t.name, delim=(None, None)):
                        self.emit('res = tmp.%s' % fmt_var(t.name))
        elif not is_void_type(route.result_data_type):
            with self.block('err = json.Unmarshal(body, &res);'
                            'if err != nil'):
                out('return')
            out()
        out('return')
