import requests
import webbrowser
import time
from urllib.parse import urlparse, parse_qs

class WcSession:
    def __init__(self, wc_base_url, auth_type, auth_params):
        self.auth_type = auth_type
        
        if self.auth_type == 'basic':
            self.api_base_url = wc_base_url + '/servlet/odata'
            self.username = auth_params[0]
            self.password = auth_params[1]
        elif self.auth_type == 'oauth':
            self.api_base_url = wc_base_url + '/oauth/servlet/odata'
            self.auth_url = auth_params[0]
            self.token_url = auth_params[1]
            self.client_id = auth_params[2]
            self.client_secret = auth_params[3]
            self.redirect_uri = auth_params[4]
            self.scope = auth_params[5]
            self.access_token = None
            self.refresh_token = None
            self.token_expiry = 0
        else:
            raise ValueError("Invalud auth type. Should be 'basic' or 'oauth'.")
        
        self.headers = {}
        self.session = requests.Session()
        self.CSRF_DOMAIN = 'PTC'
        self.CSRF_ENDPOINT = 'GetCSRFToken'

    @classmethod
    def basic(cls, wc_base_url, username, password):
        return cls(wc_base_url, auth_type='basic', auth_params=(username, password))
    
    @classmethod
    def oauth(cls, wc_base_url, auth_url, token_url, client_id, client_secret, redirect_uri, scope):
        return cls(wc_base_url, auth_type='oauth', auth_params=(auth_url, token_url, client_id, client_secret, redirect_uri, scope))
        
    def authenticate(self):
        if self.auth_type == 'basic':
            self.session.auth = (self.username, self.password)
        elif self.auth_type == 'oauth':
            auth_request_url = f'{self.auth_url}?client_id={self.client_id}&response_type=code&redirect_uri={self.redirect_uri}&scope={self.scope}'
            webbrowser.open(auth_request_url)
            print("Please authorize the application and provide the URL that you are redirected to.")
            redirect_url = input("Enter the redirect URL: ")
            qs = parse_qs(urlparse(redirect_url).query)
            auth_code = qs['code'][0]
            response = self.session.post(self.token_url, data={
                'grant_type': 'authorization_code',
                'code': auth_code,
                'redirect_uri': self.redirect_uri,
                'client_id': self.client_id,
                'client_secret': self.client_secret
            })
            if response.status_code != 200:
                raise self.session.exceptions.HTTPError(response.text)
            tokens = response.json()
            self.access_token = tokens['access_token']
            self.refresh_token = tokens['refresh_token']
            self.token_expiry = time.time() + tokens['expires_in']
            self.headers = {'Authorization': f'Bearer {self.access_token}'}
        else:
            raise ValueError("Invalud auth type. Should be 'basic' or 'oauth'.")

        response = self.call_api('get', domain=self.CSRF_DOMAIN, endpoint=self.CSRF_ENDPOINT)
        if response.status_code != 200:
            raise requests.exceptions.HTTPError(response.text)
        csrf = response.json()
        self.headers[csrf['NonceKey']] = csrf['NonceValue']

    def refresh(self):
        if self.auth_type == 'oauth' and time.time() > self.token_expiry:
            response = self.session.post(self.token_url, data={
                'grant_type': 'refresh_token',
                'refresh_token': self.refresh_token,
                'client_id': self.client_id,
                'client_secret': self.client_secret
            })
            tokens = response.json()
            self.access_token = tokens['access_token']
            self.token_expiry = time.time() + tokens['expires_in']
            self.headers['Authorization'] = f'Bearer {self.access_token}'

    def call_api(self, request_type, domain=None, endpoint=None, arguments=None, action=None, params=None, body=None, data=None, raw_url=None, addl_headers=None):
        #TODO: See if attachment upload can use json instead of data so we can remove the data from this signature
        self.refresh()
        if raw_url:
            url = raw_url
            params=None
        else:
            url = f"{self.api_base_url}/{domain}/{endpoint}"
            if arguments:
                url = url + f"(\'{arguments}\')"
            if action:
                url = url + f"/{action}"
        if addl_headers:
            headers = self.headers | addl_headers
        else:
            headers = self.headers

        if request_type.lower() == "get":
            #print(self.session.get(url, headers=self.headers, params=params).url)
            #print(params)
            #print(headers)
            response = self.session.get(url, headers=headers, params=params)
        elif request_type.lower() == "post":
            if data:
                response = self.session.post(url, headers=headers, params=params, data=data)
            else:
                response = self.session.post(url, headers=headers, params=params, json=body)
        elif request_type.lower() == "patch":
            response = self.session.patch(url, headers=headers, params=params, json=body)
        elif request_type.lower() == "delete":
            response = self.session.delete(url, headers=headers, params=params)
        else:
            raise ValueError(f"Unsupported request type {request_type}")
        
        try:
            response.raise_for_status()
        except requests.HTTPError as e:
            e.args = (f'{e.args[0]}\nError Text:\n{response.text}',)
            raise
        
        return response

    def get_objects(self, cls, filter_str="", select_str="", orderby_str="", expand_str="", params={}, page_size=None):
        #optional params: 'ptc.search.allowFilterOnTime': 'true', 'ptc.search.latestversion': 'true', 
        if filter_str:
            params["$filter"] = filter_str
        if select_str:
            params["$select"] = select_str
        if orderby_str:
            params['$orderby'] = orderby_str
        if expand_str:
            params['$expand'] = expand_str

        if page_size:
            addl_headers = {'Prefer': f'odata.maxpagesize={page_size}'}
        else:
            addl_headers = None
        
        domain = cls.domain
        endpoint = cls.endpoint

        response = self.call_api(
            request_type="get",
            domain=domain,
            endpoint=endpoint,
            params=params,
            addl_headers=addl_headers
        )
        data = response.json()
        
        for item in data.get("value", []):
            obj = cls(self, json=item)
            yield obj

        next_link = data.get("@odata.nextLink")
        while next_link:
            response = self.call_api(
                request_type="get",
                raw_url=next_link,
                addl_headers=addl_headers
            )
            data = response.json()
            
            for item in data.get("value", []):
                obj = cls(self, json=item)
                yield obj
            next_link = data.get("@odata.nextLink")

    def create_object(self, cls, attrs):
        response = self.call_api(
            request_type="post",
            domain=cls.domain,
            endpoint=cls.endpoint,
            body=attrs
        )
        obj = cls(self, json=response.json())
        return obj