from __future__ import annotations

import hashlib
import json
import os
import time
from pathlib import Path

import httpx

from .envelope import JSON_CT, request_envelope, unwrap_response

def _normalize_authorization(token: str) -> str:
  return token if token.lower().startswith('bearer ') else f'Bearer {token}'

class RefreshingTokenProvider:
  def __init__(self, refresh_token: str, token_url: str, *, timeout: float = 30.0):
    self.refresh_token = refresh_token
    self.token_url = token_url
    self.timeout = timeout
    self.access_token: str | None = None
    self.expires_at = 0.0
    self.cache_path = self._cache_path(refresh_token, token_url)
    self._load_cache()

  @staticmethod
  def _cache_path(refresh_token: str, token_url: str) -> Path:
    key = hashlib.sha256(f'{token_url}|{refresh_token}'.encode()).hexdigest()[:16]
    return Path('/tmp/.nanoshare-cli') / f'token-{key}.json'

  def __call__(self) -> str:
    if self.access_token and time.time() < self.expires_at - 60:
      return self.access_token
    headers = {'Authorization': _normalize_authorization(self.refresh_token), 'Accept': 'application/json'}
    response = httpx.post(self.token_url, headers=headers, timeout=self.timeout)
    response.raise_for_status()
    data = response.json()
    self.access_token = data['access_token']
    self.expires_at = float(data.get('expires_at') or (time.time() + int(data.get('expires_in', 3600))))
    self._store_cache()
    return self.access_token

  def _load_cache(self) -> None:
    if not self.cache_path.is_file():
      return
    try:
      data = json.loads(self.cache_path.read_text())
    except (OSError, ValueError):
      return
    if isinstance(data.get('access_token'), str):
      self.access_token = data['access_token']
      self.expires_at = float(data.get('expires_at') or 0)

  def _store_cache(self) -> None:
    try:
      self.cache_path.parent.mkdir(parents=True, exist_ok=True)
      os.chmod(self.cache_path.parent, 0o700)
      tmp = self.cache_path.with_name(f'{self.cache_path.name}.{os.getpid()}.tmp')
      tmp.write_text(json.dumps({'access_token': self.access_token, 'expires_at': self.expires_at}))
      os.chmod(tmp, 0o600)
      os.replace(tmp, self.cache_path)
    except OSError:
      pass

class NanoShareClient:
  def __init__(self, *, source: str, registry: dict[str, str], token: str | None = None, refresh_token: str | None = None, token_url: str | None = None, timeout: float = 30.0):
    self.source = source
    self.registry = {name: url.rstrip('/') for name, url in registry.items()}
    self.token = token
    self.timeout = timeout
    self.token_provider = RefreshingTokenProvider(refresh_token, token_url, timeout=timeout) if refresh_token and token_url else None
    self._client = httpx.Client(timeout=timeout)

  def close(self) -> None:
    self._client.close()

  def auth_headers(self) -> dict[str, str]:
    token = self.token or (self.token_provider() if self.token_provider else None)
    return {'Authorization': _normalize_authorization(token)} if token else {}

  def url(self, node: str, path: str = '/rpc') -> str:
    if node not in self.registry:
      raise RuntimeError(f'unknown node: {node}')
    return self.registry[node].rstrip('/') + path

  def call(self, node: str, method: str, params: dict | None = None):
    envelope = request_envelope(method, params, source=self.source, target=node)
    response = self._client.post(
      self.url(node, '/rpc'),
      json=envelope,
      headers={'Accept': JSON_CT, 'Content-Type': JSON_CT, **self.auth_headers()},
    )
    response.raise_for_status()
    return unwrap_response(response.json())
