from __future__ import annotations

from datetime import datetime
import json
import time
from typing import Any, Dict, List, Optional

from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

from .. import settings
from ..services.google_sheets_oauth import GOOGLE_SHEETS_SCOPE
from ..services.google_rate_limiter import DEFAULT_RPM_LIMIT, acquire as acquire_rate_limit


class DirectSheetsWriterError(Exception):
    pass


class RateLimitedError(DirectSheetsWriterError):
    pass


class SheetNotFoundError(DirectSheetsWriterError):
    pass


def _column_letter(idx: int) -> str:
    letters = ""
    while idx > 0:
        idx, remainder = divmod(idx - 1, 26)
        letters = chr(65 + remainder) + letters
    return letters or "A"


def _quote_sheet_name(name: str) -> str:
    safe = str(name or "").replace("'", "''")
    return f"'{safe}'"


def _http_status(err: HttpError) -> Optional[int]:
    if hasattr(err, "status_code") and err.status_code:
        return int(err.status_code)
    resp = getattr(err, "resp", None)
    if resp is not None and getattr(resp, "status", None) is not None:
        return int(resp.status)
    return None


def _http_message(err: HttpError) -> str:
    content = ""
    raw = getattr(err, "content", None)
    if raw:
        try:
            content = raw.decode("utf-8")
        except Exception:
            content = str(raw)
    if content:
        try:
            parsed = json.loads(content)
            if isinstance(parsed, dict):
                payload = parsed.get("error") if isinstance(parsed.get("error"), dict) else parsed
                message = payload.get("message") if isinstance(payload, dict) else None
                if message:
                    return str(message)
        except Exception:
            pass
    return str(err)


def _is_sheet_not_found(err: HttpError) -> bool:
    status = _http_status(err)
    if status not in (400, 404):
        return False
    message = _http_message(err).lower()
    return (
        "sheet not found" in message
        or "unable to parse range" in message
        or "not found: sheet" in message
    )


class DirectSheetsWriter:
    def __init__(
        self,
        access_token: str,
        refresh_token: Optional[str] = None,
        expires_at: Optional[datetime] = None,
        account_id: Optional[str] = None,
        rate_limit_rpm: int = DEFAULT_RPM_LIMIT,
    ) -> None:
        if not access_token:
            raise DirectSheetsWriterError("Missing Google access token")
        self._credentials = Credentials(
            token=access_token,
            refresh_token=refresh_token,
            token_uri="https://oauth2.googleapis.com/token",
            client_id=settings.GOOGLE_CLIENT_ID,
            client_secret=settings.GOOGLE_CLIENT_SECRET,
            scopes=[GOOGLE_SHEETS_SCOPE],
        )
        if expires_at:
            self._credentials.expiry = expires_at
        self._account_id = account_id
        self._rate_limit_rpm = max(1, int(rate_limit_rpm or DEFAULT_RPM_LIMIT))

    def _service(self):
        return build("sheets", "v4", credentials=self._credentials, cache_discovery=False)

    def _call_with_backoff(self, action, action_name: str):
        attempt = 0
        backoff_seconds = 1
        while True:
            if self._account_id:
                allowed = acquire_rate_limit(self._account_id, self._rate_limit_rpm)
                if not allowed:
                    raise RateLimitedError(
                        "Google Sheets rate limit reached before " + action_name
                    )
            try:
                return action()
            except HttpError as err:
                status = _http_status(err)
                if status == 429:
                    if attempt >= 3:
                        raise RateLimitedError(
                            "Google Sheets rate limit exceeded during " + action_name
                        ) from err
                    time.sleep(backoff_seconds)
                    attempt += 1
                    backoff_seconds *= 2
                    continue
                if _is_sheet_not_found(err):
                    raise SheetNotFoundError(_http_message(err)) from err
                raise DirectSheetsWriterError(_http_message(err)) from err

    def _resolve_sheet_title(
        self,
        spreadsheet_id: str,
        sheet_gid: Optional[str],
        fallback_name: Optional[str],
    ) -> str:
        service = self._service()
        meta = self._call_with_backoff(
            lambda: service.spreadsheets()
            .get(
                spreadsheetId=spreadsheet_id,
                fields="sheets.properties.sheetId,sheets.properties.title",
            )
            .execute(),
            "metadata_lookup",
        )
        sheets = meta.get("sheets", []) if isinstance(meta, dict) else []
        if sheet_gid is not None:
            for sheet in sheets:
                props = sheet.get("properties") or {}
                if str(props.get("sheetId")) == str(sheet_gid):
                    title = props.get("title")
                    if title:
                        return title
        if fallback_name:
            return fallback_name
        raise DirectSheetsWriterError("Sheet tab not found for scheduled write")

    def write_to_sheet(
        self,
        spreadsheet_id: str,
        sheet_name: Optional[str],
        headers: List[str],
        rows: List[List[Any]],
        sheet_gid: Optional[str] = None,
        cached_title: Optional[str] = None,
    ) -> Dict[str, Any]:
        if not spreadsheet_id:
            raise DirectSheetsWriterError("Missing spreadsheet_id")

        def _write_with_title(title: str) -> Dict[str, Any]:
            quoted = _quote_sheet_name(title)
            service = self._service()

            self._call_with_backoff(
                lambda: service.spreadsheets()
                .values()
                .clear(
                    spreadsheetId=spreadsheet_id,
                    range=quoted,
                    body={},
                )
                .execute(),
                "clear",
            )

            values = []
            if headers:
                values.append(headers)
            values.extend(rows or [])

            if not values:
                return {"ok": True, "rows": 0, "sheet_name": title}

            self._call_with_backoff(
                lambda: service.spreadsheets()
                .values()
                .update(
                    spreadsheetId=spreadsheet_id,
                    range=f"{quoted}!A1",
                    valueInputOption="RAW",
                    body={"values": values},
                )
                .execute(),
                "update",
            )

            return {
                "ok": True,
                "rows": len(values) - (1 if headers else 0),
                "sheet_name": title,
            }

        if cached_title:
            try:
                return _write_with_title(cached_title)
            except SheetNotFoundError:
                pass

        title = self._resolve_sheet_title(spreadsheet_id, sheet_gid, sheet_name)
        return _write_with_title(title)
