# -*- coding: utf-8 -*-

# Third Part
import re
import jwt
import json
import urllib.parse

# configuration
import configuration

# Utils
from bravaweb.utils import GetBody, RequestMethod

# Secutiry
from bravaweb.security.request_params import ValidRequestParams

# App Log
from bravaweb.utils.log import *

from bravaweb.utils.encoder import CustomEncoder

import json

class Environment():

    def __init__(self, headers, scope, receive, send):

        # Init Params
        self.headers = headers
        self.scope = scope
        self.send = send
        self.receive = receive

        # Default Params
        self.origin = headers["origin"] if "origin" in headers else None
        self.remote_ip = scope['client'][0]
        Debug(f"Remote Ip from scope: {self.remote_ip}")

        # GET REAL USER IP IF FROM CDN SERVICE
        trusted_proxies = getattr(configuration.api, "trusted_proxies", [])
        is_trusted_proxy = ('*' in trusted_proxies) or (self.remote_ip in trusted_proxies)

        if is_trusted_proxy:
            if 'cf-connecting-ip' in headers:
                self.remote_ip = headers['cf-connecting-ip']
                Debug(f"Remote Ip from 'CF-Connecting-IP' header: {self.remote_ip}")
            elif 'x-forwarded-for' in headers:
                self.remote_ip = headers['x-forwarded-for'].split(',')[0].strip() if ',' in headers['x-forwarded-for'] else headers['x-forwarded-for']
                Debug(f"Remote Ip from 'X-Forwarded-For' header: {self.remote_ip}")

        self.remote_uuid = headers['uuid'] if "uuid" in headers else None
        self.browser = headers["user-agent"] if "user-agent" in headers else None
        self.accept_encoding = headers["accept-encoding"] if "accept-encoding" in headers else None
        self.remote = {
            "ip": self.remote_ip,
            'uuid' : self.remote_uuid,
            "browser": self.browser,
            "accept": self.accept_encoding
            }
        Debug(f"Origin: {self.origin}")
        Debug(f"Remote: {json.dumps(self.remote, indent=14, sort_keys=True)}")

        self.method = RequestMethod(scope["method"])
        self.response_type = None

        # Authorization
        self.authorization = headers["authorization"] if "authorization" in headers else None

        Debug(f"Authorization: {self.authorization }")

        self.auth_token = None

        try:
            if self.authorization:
                options = {"verify_exp": True} if getattr(configuration.api, "verify_jwt_exp", False) else {}
                self.bearer = jwt.decode(
                    headers["authorization"].replace('Bearer ', ''),
                    configuration.api.token,
                    algorithms=['HS256'],
                    options=options
                )
            else:
                self.bearer = None
        except jwt.ExpiredSignatureError:
            self.bearer = None
            Debug("JWT Authorization Failed: Token Expired")
        except jwt.InvalidTokenError as e:
            self.bearer = None
            Debug(f"JWT Authorization Failed: Invalid Token - {e}")
        except Exception as e:
            self.bearer = None
            Debug(f"JWT Authorization Failed: {e}")

        # Content Length
        self.content_length = int(
            headers['content-length']) if "content-length" in headers else 0

        # Query String
        self.get = {}
        query_string = scope["query_string"].decode(configuration.api.encoding)
        if query_string:
            for item_name, item_value in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
                if "[" in item_name and "]" in item_name:
                    object_name = item_name.split("[")[0]
                    field_name = item_name.split("[")[1].replace("]", "")
                    if object_name not in self.get or not isinstance(self.get[object_name], dict):
                        self.get[object_name] = {}
                    self.get[object_name][field_name] = item_value
                else:
                    if item_name not in self.get or not isinstance(self.get[item_name], dict):
                        self.get[item_name] = item_value

        # Post
        self.post = {}

        # Validate Route
        self.controller, self.area, self.module, self.action, self.id = None, None, "default", "index", None

        # Route Rule
        RouteRule = [route for route in configuration.api.routes
                     if bool(re.match(route[1], scope["path"]))][0][0]

        Debug(f"Route Rule: {RouteRule}")

        # Request Route
        RequestRoute = scope["path"][1:].split("/")
        for index, attr in enumerate(RouteRule.split("/")):
            # If Attribute was defined
            if re.match(r"\{.*?\}", attr):
                try:
                    self.__setattr__(
                        attr[attr.find("{") + 1:attr.find("}")], RequestRoute[index])
                except IndexError:
                    pass
            # Else Pattern
            elif index == 0:
                self.controller = attr
            elif index == 1:
                self.area = attr
            elif index == 2:
                self.module = attr
            elif index == 3:
                self.action = attr
            elif index == 4:
                self.id = attr

        # Validate safe names to prevent Path Traversal
        for key in ['controller', 'area', 'module', 'action']:
            val = getattr(self, key, None)
            if val and not bool(re.match(r"^[a-zA-Z0-9_-]+$", val)):
                setattr(self, key, None)

        self.route = ["controllers"]


        if self.controller:
            self.route.append(self.controller)
        if self.area:
            self.route.append(self.area)
        if self.module:
            self.route.append(self.module)

        Debug(f"Controller: {self.controller}")
        Debug(f"Area: {self.area}")
        Debug(f"Module: {self.module}")
        Debug(f"Action: {self.action}")
        Debug(f"ID: {self.id}")

        self.klass = "{}Controller".format(self.route[-1].capitalize())

    async def ReceiveBody(self):
        self.body = await GetBody(self.receive)

        # Post
        if len(self.body) > 0:
            try:
                self.post = json.loads(
                    self.body.decode(configuration.api.encoding))
            except ValueError:
                body_string = self.body.decode(configuration.api.encoding)
                if body_string:
                    for item_name, item_value in urllib.parse.parse_qsl(body_string, keep_blank_values=True):
                        if "[" in item_name and "]" in item_name:
                            object_name = item_name.split("[")[0]
                            field_name = item_name.split("[")[1].replace("]", "")
                            if object_name not in self.post or not isinstance(self.post[object_name], dict):
                                self.post[object_name] = {}
                            self.post[object_name][field_name] = item_value
                        else:
                            if item_name not in self.post or not isinstance(self.post[item_name], dict):
                                self.post[item_name] = item_value

        # Union Get and Post Params
        self.request = {**self.get, **self.post}

        Debug(f"Request: {json.dumps(self.request, cls=CustomEncoder, indent=14, sort_keys=True)}")

    async def Response(self):

        _class_ref, _class, _action, params = None, None, None, {}

        # Load Class from Router Cache
        try:
            from bravaweb.router import Router
            Debug(f"Catching Class via Router: '{'.'.join(self.route)}' -> '{self.klass}'")
            _class_ref = Router.get_controller_class(self.route, self.klass)
        except Exception as e:
            Error("Load Class via Router", e)
            raise e

        if _class_ref is None:
            Debug(f"Class not found/mapped: '{self.klass}'")
            _action = __import__("bravaweb.controller").__getattribute__("Controller")(self).__getattribute__("NotFound")
        else:
            try:
                try:
                    _class = _class_ref(environment=self)
                except TypeError:
                    try:
                        _class = _class_ref(envirom=self)
                    except TypeError:
                        try:
                            _class = _class_ref(enviroment=self)
                        except TypeError:
                            _class = _class_ref(self)
            except Exception as e:
                Error("Instancing Class", e)
                raise e

        # Load Action
        if _class:
            try:
                Debug(f"Catching Action: 'async def {self.action}(...'")
                _action = _class.__getattribute__(self.action)

                is_safe = False
                _current_action = _action
                _visited_actions = set()
                while _current_action and id(_current_action) not in _visited_actions:
                    _visited_actions.add(id(_current_action))
                    if getattr(_current_action, "is_route", False):
                        is_safe = True
                        break
                    
                    if hasattr(_current_action, "__wrapped__"):
                        _current_action = _current_action.__wrapped__
                        continue
                        
                    if hasattr(_current_action, "__func__"):
                        _current_action = _current_action.__func__
                        continue
                        
                    if hasattr(_current_action, "__annotations__") and "return" in _current_action.__annotations__:
                        _ret = _current_action.__annotations__["return"]
                        if callable(_ret):
                            _current_action = _ret
                            continue
                            
                    break
                    
                if not is_safe:
                    Debug(f"Security: Action '{self.action}' is not marked as a safe route")
                    raise AttributeError("Action is not exposed via route decorators")

                # Validate Request Action
                try:
                    if _action:
                        Debug(f"Validating Required Parameters")
                        params, errors = ValidRequestParams(self, _action, _class)
                        if len(errors) > 0:
                            Debug(f"Validate fail: {errors}")
                            _action = _class.__getattribute__("PreconditionFailed")
                            params = {'errors': errors}
                except Exception as e:
                    Error("Validade Request Params", e)
                    raise e

            except AttributeError as e:
                Error("Attribute Params Error", e)
                # 404 Not Found
                Debug(f"Action not found: 'async def {self.action}(...)")
                _action = __import__("bravaweb.controller").__getattribute__("Controller")(self).__getattribute__("NotFound")
            except Exception as e:
                _action = __import__("bravaweb.controller").__getattribute__("Controller")(self).__getattribute__("InternalError")
                await _action(e)

        # Execute Action
        try:
            Debug(f"Execute Action {self.action}")
            await _action(**params)
        except Exception as e:
            Error("Execute Action", e)
            raise e
