"""
Title: TalentDesk API Client
Description:
    Core API client for interacting with the TalentDesk REST API.
    Handles authentication, session management, error handling, retries,
    and generic request dispatching across all modules (users, expenses, projects, etc.).
Author: Scott Murray
Version: 1.0.0
"""

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import re
from .expenses import ExpensesAPI
from .users import UserAPI
from .opportunities import OpportunitiesAPI
from .organizations import OrganizationsAPI
from .tasks import TasksAPI
from .projects import ProjectsAPI
from .invoices import InvoicesAPI
from .files import FilesAPI
from .pro_forma_invoices import ProFormaInvoicesAPI
from .worksheets import WorksheetsAPI
from .webhooks import WebhooksAPI


########################################################################################################################
########################################################################################################################
class TalentDeskClient:

    def __init__(self, token: str, mode='production', timeout=10, max_retries=3, backoff_factor=0.3) -> None:
        """
        Initialise the API client and set default attributes
        :param token: API Token
        :param: mode: production or development. Production is the default. Used to determine which API environment to use.
        :param timeout: Set to 10 seconds as the default
        :param max_retries: Will attempt 3 requests before returning an error
        :param backoff_factor: Exponential backoff. Increase to extend the time between retries. 0.3 is the default
        """
        self.base_url = "https://api.talentdesk.io/"
        if mode == 'development':
            self.base_url = "https://api-demo.talentdesk.dev/"
        self.token = token
        self.timeout = timeout
        self.session = self._init_session(max_retries, backoff_factor)
        self.expenses = ExpensesAPI(self)
        self.users = UserAPI(self)
        self.opportunities = OpportunitiesAPI(self)
        self.organizations = OrganizationsAPI(self)
        self.tasks = TasksAPI(self)
        self.projects = ProjectsAPI(self)
        self.invoices = InvoicesAPI(self)
        self.files = FilesAPI(self)
        self.proforma_invoices = ProFormaInvoicesAPI(self)
        self.worksheets = WorksheetsAPI(self)
        self.webhooks = WebhooksAPI(self)
        print(f"Running in {mode} mode.")
        print(f"API Endpoint: {self.base_url}")

########################################################################################################################
    @staticmethod
    def _init_session(max_retries: int, backoff_factor: float) -> requests.Session():
        """
         Configure a requests session with retries.
        :param max_retries: Maximum number of retries. Defaults to 3.
        :param backoff_factor: Backoff factor for retries. Defaults to 0.3.
        :return: Requests Session
        """
        session = requests.Session()
        retries = Retry(
            total=max_retries,
            backoff_factor=backoff_factor,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
        )
        adapter = HTTPAdapter(max_retries=retries)
        session.mount('http://', adapter)
        session.mount('https://', adapter)
        return session

########################################################################################################################
    def _get_auth_headers(self, existing_headers: dict = None) -> dict:
        """
        Return authorization and default headers.
        Includes 'Accept: application/json' always.
        Only includes 'Content-Type: application/json' if not already provided.
        """
        headers = {
            "X-Talentdesk-Api-Key": self.token,
            "Accept": "application/json"
        }

        # Respect user-supplied Content-Type (e.g., for file uploads)
        if not existing_headers or "Content-Type" not in existing_headers:
            headers["Content-Type"] = "application/json"

        return headers

########################################################################################################################
    def _parse_link_header(self, link_header: str) -> (str, None):
        """
        Parse the 'Link' header to extract the URL with rel="next".
        Rewrites UI URLs to use API base if needed.
        :param link_header: Link header string
        :return: URL for the next page or None
        """
        if not link_header:
            return None

        links = {}
        # Split the header into individual link segments by commas that separate links
        segments = [seg.strip() for seg in link_header.split(",") if seg.strip()]
        for seg in segments:
            # Ensure segment starts with '<url>'; remainder contains params like ; rel="next"; type="application/json"
            m = re.match(r'\s*<([^>]+)>\s*(.*)', seg)
            if not m:
                continue
            url = m.group(1).strip()
            params_str = m.group(2) or ""

            # Find rel=... (case-insensitive), allow optional quotes and trailing params
            m_rel = re.search(r'rel\s*=\s*"?([^";,]+)"?', params_str, flags=re.IGNORECASE)
            if not m_rel:
                continue
            rel = m_rel.group(1).strip().lower()

            # Rewrite frontend/demo URLs to match the API base
            if "app.talentdesk.io" in url and "api.talentdesk.io" in self.base_url:
                url = url.replace("app.talentdesk.io", "api.talentdesk.io")
            if "demo.talentdesk.dev" in url and "api-demo.talentdesk.dev" in self.base_url:
                url = url.replace("demo.talentdesk.dev", "api-demo.talentdesk.dev")

            links[rel] = url

        return links.get("next")

########################################################################################################################
    def _build_url(self, base_endpoint: str, next_url: str) -> str:
        """
        Build the full request URL based on the next page information.
        :param base_endpoint: The base API endpoint.
        :param next_url: The next page query string or absolute URL.
        :return: Full URL for the next request.
        """
        if next_url.startswith('http'):
            return next_url
        base = self.base_url.rstrip('/')
        if next_url.startswith('?'):
            return f"{base}/{base_endpoint}{next_url}"
        return f"{base}/{next_url.lstrip('/')}"

########################################################################################################################
    def _extract_message(self, response: requests.models.Response) -> str:
        """
        Helper method to gracefully extract messages from API responses
        :param response: API Response
        :return: String containing response message
        """
        try:
            error_info = response.json()
            return (
                    error_info.get("_meta", {}).get("message")
                    or error_info.get("_error", "")
                    or response.reason
            )
        except Exception:
            return response.text[:200] or "An unknown error occurred"

########################################################################################################################
    def _handle_response(self, response: requests.models.Response) -> [list, dict]:
        """
        Normalize API responses and handle errors.
        :param response: API Response
        :return: Normalized Dictionary of results:
        {"success": True/False, "data": json data / None, "error": Error Message / None}
        """
        try:
            response.raise_for_status()
            try:
                json_data = response.json()
            except ValueError:
                return {"success": False, "data": None, "error": "Response was not valid JSON"}

            return {"success": True, "data": json_data, "error": None}

        except requests.exceptions.HTTPError:
            return {"success": False, "data": None, "error": self._extract_message(response)}

        except requests.exceptions.Timeout:
            return {"success": False, "data": None, "error": f"Request to {response.url} timed out after {self.timeout}s"}

        except requests.exceptions.RequestException as err:
            return {"success": False, "data": None, "error": f"Request failed: {err}"}

########################################################################################################################
    def _request(self, method: str, endpoint: str, **kwargs) -> (list, dict):
        """
        Perform an HTTP request and handle pagination using the Link header.
        Returns:
        - list for collection endpoints (aggregated if auto-paginating)
        - wrapper dict {"success","data","error"} for single-object endpoints
        """
        explicit_paginate = kwargs.pop("paginate", None)
        params = kwargs.get("params") if isinstance(kwargs.get("params"), dict) else {}
        has_explicit_page = isinstance(params, dict) and ("page" in params)
        auto_paginate = explicit_paginate if explicit_paginate is not None else (not has_explicit_page)

        all_data = []
        next_url = endpoint
        base_endpoint = endpoint.lstrip('/').split('?')[0]
        first_page = True

        while next_url:
            url = self._build_url(base_endpoint, next_url)

            headers = kwargs.get("headers", {}) or {}
            headers = {**headers}
            headers.update(self._get_auth_headers(headers))

            request_kwargs = {k: v for k, v in kwargs.items() if k != "headers"}
            if not first_page and "params" in request_kwargs:
                request_kwargs = {k: v for k, v in request_kwargs.items() if k != "params"}

            response = self.session.request(method, url, timeout=self.timeout, headers=headers, **request_kwargs)
            json_response = self._handle_response(response)
            if not json_response.get("success"):
                print(f"[ERROR] {json_response.get('error')}")
                return json_response

            payload = json_response.get("data")
            link_header = response.headers.get("Link")
            next_link = self._parse_link_header(link_header)

            # Case 1: top-level list
            if isinstance(payload, list):
                if not auto_paginate:
                    return payload
                all_data.extend(payload)
                first_page = False
                if next_link:
                    next_url = next_link
                    continue
                break

            # Case 2: dict payload
            if isinstance(payload, dict):
                # Only treat dict-with-list as a collection page if Link header indicates pagination
                page_items = None
                if next_link:
                    for value in payload.values():
                        if isinstance(value, list):
                            page_items = value
                            break

                if page_items is not None:
                    if not auto_paginate:
                        return page_items
                    all_data.extend(page_items)
                    first_page = False
                    if next_link:
                        next_url = next_link
                        continue
                    break

                # Single-object response: return wrapper for compatibility
                return json_response

            # Fallback: return wrapper as-is
            return json_response

        return all_data

########################################################################################################################
########################################################################################################################
