import requests
import json
from datetime import datetime

class DragXcel:
    """
    DragXcel SDK
    Official Python client for the DragXcel API Platform.
    """
    
    DEFAULT_BASE_URL = "https://dragxcel.vercel.app"

    def __init__(self, server_key=None, base_url=None, smtp=None):
        """
        Initialize the DragXcel SDK.
        
        :param server_key: Your Server-End API Key (dxl_srv_*)
        :param base_url: Custom API base URL (default: https://dragxcel.vercel.app)
        :param smtp: Default SMTP accounts for sending
        """
        self.server_key = server_key
        self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
        self.smtp = smtp

    def _request(self, endpoint, method="POST", body=None, use_v1=True, custom_headers=None):
        api_path = "/api/v1" if use_v1 else "/api"
        url = f"{self.base_url}{api_path}{endpoint}"
        
        headers = {
            "Content-Type": "application/json"
        }
        if custom_headers:
            headers.update(custom_headers)

        if self.server_key and "x-server-key" not in headers and "x-user-key" not in headers:
            headers["x-server-key"] = self.server_key

        try:
            response = requests.request(
                method=method,
                url=url,
                headers=headers,
                data=json.dumps(body) if body else None
            )
            data = response.json()
            
            if not response.ok:
                error_msg = data.get("error", f"API Error (HTTP {response.status_code})")
                raise Exception(error_msg)
                
            return data
        except requests.exceptions.RequestException as e:
            raise Exception(f"Connection failed: {str(e)}")

    def connect(self, user_key, protection_key, endpoint):
        """
        Connect your external data source to DragXcel.
        """
        if not user_key or not protection_key or not endpoint:
            raise ValueError("DragXcel: 'user_key', 'protection_key', and 'endpoint' are required.")

        return self._request("/keys/bridge/connect", "POST", {
            "userKey": user_key,
            "protectionKey": protection_key,
            "endpoint": endpoint
        }, use_v1=False)

    def generate(self, filter_rules=None, format="png"):
        """
        Generate certificates/documents for all (or filtered) records.
        """
        return self._request("/generate", "POST", {
            "filter": filter_rules or [],
            "format": format
        })

    def generate_one(self, data, format="png"):
        """
        Generate a single certificate for a specific data payload.
        """
        if data is None:
            raise ValueError("DragXcel: 'data' dict is required for generate_one().")
            
        return self._request("/generate/one", "POST", {
            "data": data,
            "format": format
        })

    def send(self, subject="DragXcel Certificate", filter_rules=None, smtp=None):
        """
        Generate and send personalized certificates/emails via SMTP.
        """
        return self._request("/send", "POST", {
            "subject": subject,
            "filter": filter_rules or [],
            "smtp": smtp or self.smtp
        })

    def status(self, job_id):
        """
        Check the status of a bulk generation/send job.
        """
        if not job_id:
            raise ValueError("DragXcel: 'job_id' is required.")
            
        return self._request(f"/status/{job_id}", "GET")

    def get_template(self):
        """
        Get the current template configuration for this key.
        """
        return self._request("/template", "GET")

    @staticmethod
    def filter_data(data, rules=None):
        """
        Filter a list of dictionaries based on DragXcel filter rules.
        """
        if not isinstance(data, list):
            return []
        if not rules:
            return data

        filtered = []
        for item in data:
            match_all = True
            for rule in rules:
                col = rule.get('column')
                op = rule.get('operator')
                f_val = rule.get('value')
                
                item_val = item.get(col)
                if item_val is None:
                    match_all = False
                    break
                
                val_str = str(item_val).lower()
                f_val_str = str(f_val).lower()
                
                if op == 'equals':
                    if val_str != f_val_str: match_all = False
                elif op == 'contains':
                    if f_val_str not in val_str: match_all = False
                elif op == 'starts_with':
                    if not val_str.startswith(f_val_str): match_all = False
                elif op == 'ends_with':
                    if not val_str.endswith(f_val_str): match_all = False
                elif op == 'is_empty':
                    if str(item_val).strip() != "": match_all = False
                elif op == 'is_not_empty':
                    if str(item_val).strip() == "": match_all = False
                elif op in ['gt', 'greater_than']:
                    if not float(item_val) > float(f_val): match_all = False
                elif op in ['lt', 'less_than']:
                    if not float(item_val) < float(f_val): match_all = False
                elif op in ['gte', 'greater_or_equal']:
                    if not float(item_val) >= float(f_val): match_all = False
                elif op in ['lte', 'less_or_equal']:
                    if not float(item_val) <= float(f_val): match_all = False
                elif op == 'is_after':
                    if not datetime.fromisoformat(str(item_val)) > datetime.fromisoformat(str(f_val)): 
                        match_all = False
                elif op == 'is_before':
                    if not datetime.fromisoformat(str(item_val)) < datetime.fromisoformat(str(f_val)): 
                        match_all = False
                
                if not match_all: break
            
            if match_all:
                filtered.append(item)
                
        return filtered
