import requests
from urllib.parse import urljoin

class OctoPrintClient:
    """
    Client for interacting with the OctoPrint API using Application Keys or standard API Keys.
    Designed to be easily extensible.
    """
    def __init__(self, base_url, api_key=None):
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.session = requests.Session()
        if self.api_key:
            self.session.headers.update({"X-Api-Key": self.api_key})

    def _request(self, method, endpoint, **kwargs):
        """Internal method to handle API requests and standard parsing."""
        url = urljoin(self.base_url, endpoint)
        response = self.session.request(method, url, **kwargs)
        response.raise_for_status()
        
        # Octoprint often returns 204 No Content for successful commands
        if response.status_code == 204:
            return True
            
        try:
            return response.json()
        except ValueError:
            return response.text

    # -------------------------------------------------------------------------
    # Application Key Workflow
    # -------------------------------------------------------------------------
    def request_app_key(self, app_name="My OctoPrint App"):
        """
        Step 1: Start the Application Key authorization flow.
        Returns: (app_token, client_token)
        You should poll verify_app_key(client_token) until the user allows it in OctoPrint UI.
        """
        data = self._request("POST", "/plugin/appkeys/request", json={"app": app_name})
        return data.get("app_token"), data.get("client_token")

    def verify_app_key(self, client_token):
        """
        Step 2: Check if the requested app key is approved.
        Returns True if authorized, False if still pending.
        Raises an exception if denied or timed out.
        """
        url = urljoin(self.base_url, "/plugin/appkeys/probe")
        # App Key probe requires passing the client_token as a Bearer token
        # Don't use self._request since it uses the established session/api_key
        response = requests.get(url, headers={"Authorization": f"Bearer {client_token}"})
        
        if response.status_code == 204:
            return True
        elif response.status_code == 202:
            return False  # Pending approval
        else:
            raise Exception(f"App key request denied or expired (HTTP {response.status_code})")

    # -------------------------------------------------------------------------
    # Files
    # -------------------------------------------------------------------------
    def get_files(self, location="local"):
        """
        Retrieve all files available on OctoPrint.
        location: 'local' (internal storage) or 'sdcard' (SD card on printer)
        """
        return self._request("GET", f"/api/files/{location}")

    def select_file(self, location, path, print_after_select=False):
        """Select a file, and optionally start printing it immediately."""
        payload = {"command": "select", "print": print_after_select}
        return self._request("POST", f"/api/files/{location}/{path}", json=payload)

    def upload_file(self, location, path_to_file, filename_override=None):
        """Upload a file to OctoPrint"""
        with open(path_to_file, 'rb') as f:
            files = {'file': (filename_override or path_to_file.split('/')[-1], f, 'application/octet-stream')}
            url = urljoin(self.base_url, f"/api/files/{location}")
            response = self.session.post(url, files=files)
            response.raise_for_status()
            if response.status_code == 204:
                return True
            try:
                return response.json()
            except ValueError:
                return response.text

    def delete_file(self, location, path):
        """Delete a file from OctoPrint"""
        return self._request("DELETE", f"/api/files/{location}/{path}")

    # -------------------------------------------------------------------------
    # Printer Status
    # -------------------------------------------------------------------------
    def get_printer_status(self):
        """
        Get the current printer state (e.g., temperatures, operational state).
        Note: If printer is disconnected, this may return an HTTP error.
        """
        try:
            return self._request("GET", "/api/printer")
        except requests.HTTPError as e:
            if e.response.status_code == 409:
                return {"state": {"text": "Offline/Disconnected"}}
            raise

    def get_job_info(self):
        """Get information about the current print job and progress."""
        return self._request("GET", "/api/job")

    # -------------------------------------------------------------------------
    # Printer Control
    # -------------------------------------------------------------------------
    def start_print(self):
        """Start the print job (Requires a file to be selected first)."""
        return self._request("POST", "/api/job", json={"command": "start"})

    def pause_print(self, action="pause"):
        """
        Pause, resume, or toggle the print job.
        action: 'pause', 'resume', or 'toggle'
        """
        return self._request("POST", "/api/job", json={"command": "pause", "action": action})

    def cancel_print(self):
        """Cancel the current print job."""
        return self._request("POST", "/api/job", json={"command": "cancel"})

    def send_gcode(self, commands):
        """
        Send a string or list of G-Code commands directly to the printer.
        Example: send_gcode("G28") or send_gcode(["G28", "G29"])
        """
        if isinstance(commands, str):
            commands = [commands]
        return self._request("POST", "/api/printer/command", json={"commands": commands})
        
    def home_axes(self, axes=["x", "y", "z"]):
        """Convenience method to home the printer axes."""
        return self._request("POST", "/api/printer/printhead", json={"command": "home", "axes": axes})

    # -------------------------------------------------------------------------
    # Webcam / Video
    # -------------------------------------------------------------------------
    def get_webcam_stream_url(self):
        """
        Attempts to fetch the configured webcam stream URL from OctoPrint settings.
        Provides a fallback if settings are inaccessible.
        """
        try:
            settings = self._request("GET", "/api/settings")
            stream_url = settings.get("webcam", {}).get("streamUrl", "/webcam/?action=stream")
            if stream_url.startswith("/"):
                return urljoin(self.base_url, stream_url)
            return stream_url
        except requests.HTTPError:
            # Fallback standard URL
            return urljoin(self.base_url, "/webcam/?action=stream")

# --- Example Usage / Extensibility test ---
if __name__ == "__main__":
    # Example snippet of how to use the client:
    OCTOPRINT_URL = "http://octopi.local"
    # OCTOPRINT_KEY = "YOUR_APP_KEY_HERE"
    
    # client = OctoPrintClient(OCTOPRINT_URL, OCTOPRINT_KEY)
    # print(client.get_printer_status())
    pass
