"""Odoo REST backend — wraps REST API calls to a running Odoo instance.

Uses OAuth2 for authentication (client_credentials grant) and
JSON/HTTP for all operations.  No external dependencies — uses only stdlib.
"""

import base64
import json
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any

from cli_anything.odoo_12.utils.odoo_backend_base import OdooBackend


class OdooREST(OdooBackend):
    """REST API client for Odoo.

    All operations use HTTP/JSON through the REST API endpoints.
    Uses OAuth2 ``client_credentials`` grant type.
    """

    TOKEN_BUFFER_SECONDS = 60

    def __init__(self, url: str, db: str, user: str, password: str,
                 client_id: str | None = None, client_secret: str | None = None,
                 access_token: str | None = None, refresh_token: str | None = None,
                 token_expiry: float | None = None,
                 grant_type: str = "client_credentials"):
        super().__init__(url, db, user, password)
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token = access_token
        self.refresh_token = refresh_token
        self.token_expiry = token_expiry
        self.grant_type = grant_type

    # ── HTTP helper ──────────────────────────────────────────────

    def _request(self, method: str, path: str, params: dict | None = None,
                 body: dict | None = None, auth: bool = True,
                 binary_response: bool = False) -> Any:
        """Make an HTTP request to the REST API.

        Args:
            method: HTTP method (GET, POST, PUT, DELETE)
            path: API path (e.g., /api/search/res.partner)
            params: Query string parameters
            body: JSON request body
            auth: Whether to include Bearer token
            binary_response: If True, return raw bytes instead of JSON
        """
        if auth:
            self._ensure_auth()

        url = self.url + path
        if params:
            encoded = {}
            for k, v in params.items():
                if isinstance(v, (list, dict)):
                    encoded[k] = json.dumps(v)
                elif isinstance(v, bool):
                    encoded[k] = "true" if v else "false"
                else:
                    encoded[k] = str(v)
            url += "?" + urllib.parse.urlencode(encoded)

        data = None
        if body is not None:
            data = json.dumps(body).encode("utf-8")

        headers = {}
        if data is not None:
            headers["Content-Type"] = "application/json"
        if auth and self.access_token:
            headers["Authorization"] = f"Bearer {self.access_token}"

        req = urllib.request.Request(url, data=data, headers=headers, method=method)

        try:
            with urllib.request.urlopen(req) as resp:
                if binary_response:
                    return resp.read()
                raw = resp.read().decode("utf-8")
                if not raw:
                    return None
                return json.loads(raw)
        except urllib.error.HTTPError as e:
            if e.code == 401 and auth and self.refresh_token:
                # Try refreshing token and retry once
                self._refresh_access_token()
                headers["Authorization"] = f"Bearer {self.access_token}"
                req = urllib.request.Request(url, data=data, headers=headers, method=method)
                try:
                    with urllib.request.urlopen(req) as resp:
                        if binary_response:
                            return resp.read()
                        raw = resp.read().decode("utf-8")
                        if not raw:
                            return None
                        return json.loads(raw)
                except urllib.error.HTTPError as e2:
                    error_body = ""
                    try:
                        error_body = e2.read().decode("utf-8")
                    except Exception:
                        pass
                    raise RuntimeError(
                        f"REST API error {e2.code} on {method} {path}: {error_body}"
                    ) from e2
            else:
                error_body = ""
                try:
                    error_body = e.read().decode("utf-8")
                except Exception:
                    pass
                raise RuntimeError(
                    f"REST API error {e.code} on {method} {path}: {error_body}"
                ) from e
        except urllib.error.URLError as e:
            raise RuntimeError(
                f"Cannot connect to Odoo REST API at {self.url}\n"
                f"Error: {e}\n\n"
                f"Make sure the Odoo server is running and muk_rest is installed."
            ) from e

    # ── Token management ─────────────────────────────────────────

    def authenticate(self) -> int:
        """Authenticate via OAuth2 client_credentials. Returns uid."""
        if not self.client_id or not self.client_secret:
            raise RuntimeError(
                "REST backend requires client_id and client_secret.\n"
                "Configure with: config setup-rest --client-id <id> --client-secret <secret>"
            )

        token_url = self.url + "/api/authentication/oauth2/token"

        form_fields = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "db": self.db,
        }

        form_data = urllib.parse.urlencode(form_fields).encode("utf-8")
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        req = urllib.request.Request(token_url, data=form_data, headers=headers, method="POST")

        try:
            with urllib.request.urlopen(req) as resp:
                result = json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            error_body = ""
            try:
                error_body = e.read().decode("utf-8")
            except Exception:
                pass
            raise RuntimeError(
                f"OAuth2 authentication failed (grant_type={self.grant_type}) "
                f"on database '{self.db}'.\n"
                f"HTTP {e.code}: {error_body}\n"
                f"Check credentials, client_id, and client_secret."
            ) from e
        except urllib.error.URLError as e:
            raise RuntimeError(
                f"Cannot connect to Odoo at {self.url}\n"
                f"Error: {e}\n\n"
                f"Make sure the Odoo server is running:\n"
                f"  docker-compose up -d"
            ) from e

        self.access_token = result["access_token"]
        self.refresh_token = result.get("refresh_token")
        expires_in = result.get("expires_in", 3600)
        self.token_expiry = time.time() + expires_in

        # Resolve uid via /api/user (works for any grant type)
        try:
            info = self._request("GET", "/api/user")
            if isinstance(info, dict) and info.get("uid"):
                self.uid = info["uid"]
            else:
                self.uid = 1
        except Exception:
            self.uid = 1

        return self.uid

    def _refresh_access_token(self):
        """Refresh the access token using the refresh token."""
        if not self.refresh_token:
            raise RuntimeError("No refresh token available. Re-authenticate.")

        token_url = self.url + "/api/authentication/oauth2/token"
        form_data = urllib.parse.urlencode({
            "grant_type": "refresh_token",
            "refresh_token": self.refresh_token,
            "client_id": self.client_id,
            "client_secret": self.client_secret,
        }).encode("utf-8")

        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        req = urllib.request.Request(token_url, data=form_data, headers=headers, method="POST")

        try:
            with urllib.request.urlopen(req) as resp:
                result = json.loads(resp.read().decode("utf-8"))
        except (urllib.error.HTTPError, urllib.error.URLError) as e:
            raise RuntimeError(f"Token refresh failed: {e}") from e

        self.access_token = result["access_token"]
        self.refresh_token = result.get("refresh_token", self.refresh_token)
        expires_in = result.get("expires_in", 3600)
        self.token_expiry = time.time() + expires_in

    def _ensure_auth(self):
        """Ensure we have a valid token, refreshing or re-authenticating."""
        if self.access_token is None:
            self.authenticate()
            return
        if self.token_expiry and time.time() >= (self.token_expiry - self.TOKEN_BUFFER_SECONDS):
            if self.refresh_token:
                self._refresh_access_token()
            else:
                self.authenticate()

    # ── Generic execution ────────────────────────────────────────

    def execute_kw(self, model: str, method: str, args: list,
                   kwargs: dict | None = None) -> Any:
        """Execute a method on an Odoo model via REST /api/call endpoint."""
        body = {"args": args}
        if kwargs:
            body["kwargs"] = kwargs
        return self._request("POST", f"/api/call/{model}/{method}", body=body)

    # ── Record CRUD ──────────────────────────────────────────────

    def search(self, model: str, domain: list, limit: int = 0,
               offset: int = 0, order: str = "") -> list[int]:
        params: dict[str, Any] = {"domain": domain}
        if limit:
            params["limit"] = limit
        if offset:
            params["offset"] = offset
        if order:
            params["order"] = order
        result = self._request("GET", f"/api/search/{model}", params=params)
        if isinstance(result, list):
            return result
        return result if result else []

    def search_read(self, model: str, domain: list, fields: list[str] | None = None,
                    limit: int = 0, offset: int = 0, order: str = "") -> list[dict]:
        params: dict[str, Any] = {"domain": domain}
        if fields:
            params["fields"] = fields
        if limit:
            params["limit"] = limit
        if offset:
            params["offset"] = offset
        if order:
            params["order"] = order
        result = self._request("GET", f"/api/search_read/{model}", params=params)
        if isinstance(result, list):
            return result
        return result if result else []

    def read(self, model: str, ids: list[int],
             fields: list[str] | None = None) -> list[dict]:
        params: dict[str, Any] = {"ids": ids}
        if fields:
            params["fields"] = fields
        result = self._request("GET", f"/api/read/{model}", params=params)
        if isinstance(result, list):
            return result
        return result if result else []

    def create(self, model: str, values: dict) -> int:
        result = self._request("POST", f"/api/create/{model}", body={"values": values})
        if isinstance(result, int):
            return result
        if isinstance(result, dict):
            return result.get("id", result.get("result", 0))
        return result

    def write(self, model: str, ids: list[int], values: dict) -> bool:
        result = self._request("PUT", f"/api/write/{model}",
                               body={"ids": ids, "values": values})
        return bool(result) if result is not None else True

    def unlink(self, model: str, ids: list[int]) -> bool:
        result = self._request("DELETE", f"/api/unlink/{model}",
                               body={"ids": ids})
        return bool(result) if result is not None else True

    def search_count(self, model: str, domain: list) -> int:
        params: dict[str, Any] = {"domain": domain, "count": True}
        result = self._request("GET", f"/api/search/{model}", params=params)
        if isinstance(result, int):
            return result
        return result if result else 0

    def fields_get(self, model: str, attributes: list[str] | None = None) -> dict:
        params: dict[str, Any] = {}
        if attributes:
            params["attributes"] = attributes
        result = self._request("GET", f"/api/fields/{model}", params=params)
        return result if isinstance(result, dict) else {}

    # ── Module operations ────────────────────────────────────────

    def module_list(self, state: str | None = None) -> list[dict]:
        domain: list = []
        if state:
            domain.append(("state", "=", state))
        return self.search_read(
            "ir.module.module", domain,
            fields=["name", "shortdesc", "state", "installed_version", "summary"],
            order="name",
        )

    def module_info(self, name: str) -> dict | None:
        results = self.search_read(
            "ir.module.module",
            [("name", "=", name)],
            fields=["name", "shortdesc", "state", "installed_version",
                    "summary", "description", "author", "website",
                    "category_id", "dependencies_id"],
        )
        return results[0] if results else None

    def module_install(self, name: str) -> dict:
        mods = self.search("ir.module.module", [("name", "=", name)])
        if not mods:
            raise RuntimeError(f"Module '{name}' not found")
        self.execute_kw("ir.module.module", "button_immediate_install", [mods])
        return {"module": name, "action": "install", "status": "done"}

    def module_update(self, name: str) -> dict:
        mods = self.search("ir.module.module", [("name", "=", name)])
        if not mods:
            raise RuntimeError(f"Module '{name}' not found")
        self.execute_kw("ir.module.module", "button_immediate_upgrade", [mods])
        return {"module": name, "action": "update", "status": "done"}

    # ── Database operations ──────────────────────────────────────

    def db_list(self) -> list[str]:
        try:
            result = self._request("GET", "/api/database/list", auth=False)
            if isinstance(result, list):
                return result
            if isinstance(result, dict):
                return result.get("databases", result.get("result", []))
            return []
        except Exception as e:
            raise RuntimeError(f"Cannot list databases: {e}") from e

    def db_create(self, db_name: str, admin_password: str = "admin",
                  demo: bool = False, lang: str = "pt_BR") -> bool:
        try:
            self._request("POST", "/api/database/create", auth=False, body={
                "master_password": admin_password,
                "database_name": db_name,
                "demo": demo,
                "lang": lang,
                "admin_password": admin_password,
            })
            return True
        except Exception as e:
            raise RuntimeError(f"Cannot create database '{db_name}': {e}") from e

    def db_drop(self, db_name: str, admin_password: str = "admin") -> bool:
        try:
            self._request("POST", "/api/database/drop", auth=False, body={
                "master_password": admin_password,
                "database_name": db_name,
            })
            return True
        except Exception as e:
            raise RuntimeError(f"Cannot drop database '{db_name}': {e}") from e

    def db_backup(self, db_name: str, output_path: str,
                  admin_password: str = "admin",
                  backup_format: str = "zip") -> dict:
        try:
            raw = self._request("POST", "/api/database/backup", auth=False,
                                body={
                                    "master_password": admin_password,
                                    "database_name": db_name,
                                    "backup_format": backup_format,
                                },
                                binary_response=True)
            Path(output_path).parent.mkdir(parents=True, exist_ok=True)
            with open(output_path, "wb") as f:
                f.write(raw)
            return {
                "database": db_name,
                "output": output_path,
                "size": len(raw),
                "format": backup_format,
            }
        except RuntimeError:
            raise
        except Exception as e:
            raise RuntimeError(f"Backup failed for '{db_name}': {e}") from e

    def db_restore(self, db_name: str, backup_path: str,
                   admin_password: str = "admin") -> bool:
        try:
            with open(backup_path, "rb") as f:
                backup_data = base64.b64encode(f.read()).decode()
            self._request("POST", "/api/database/restore", auth=False, body={
                "master_password": admin_password,
                "database_name": db_name,
                "backup_file": backup_data,
            })
            return True
        except RuntimeError:
            raise
        except Exception as e:
            raise RuntimeError(f"Restore failed for '{db_name}': {e}") from e

    # ── Report generation ────────────────────────────────────────

    def render_report(self, report_name: str, record_ids: list[int],
                      output_path: str) -> dict:
        """Render a report via the REST report endpoint."""
        self._ensure_auth()
        try:
            params = {"ids": record_ids, "file_response": True}
            raw = self._request("GET", f"/api/report/{report_name}/pdf",
                                params=params, binary_response=True)
            Path(output_path).parent.mkdir(parents=True, exist_ok=True)
            with open(output_path, "wb") as f:
                f.write(raw)
            return {
                "report": report_name,
                "record_ids": record_ids,
                "output": output_path,
                "size": len(raw),
            }
        except RuntimeError:
            raise
        except Exception as e:
            raise RuntimeError(f"Report render failed: {e}") from e

    # ── Server info ──────────────────────────────────────────────

    def server_version(self) -> str:
        try:
            result = self._request("GET", "/api/", auth=True)
            if isinstance(result, dict):
                return result.get("server_version", result.get("version", str(result)))
            return str(result)
        except Exception as e:
            raise RuntimeError(f"Cannot get server version: {e}") from e

    def check_connection(self) -> dict:
        """Check if Odoo is reachable and authenticate."""
        version = self.server_version()
        return {
            "url": self.url,
            "database": self.db,
            "user": self.user,
            "uid": self.uid,
            "server_version": version,
            "backend": "rest",
        }
