from __future__ import annotations

import secrets
import threading
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit

import httpx

from .config import DEFAULT_CONFIG, write_config

def _make_authorize_url(base_url: str, callback_url: str, state: str, scope: str) -> str:
  url_parts = urlsplit(f'{base_url.rstrip("/")}/auth/cli/login')
  query = dict(parse_qs(url_parts.query))
  flat_query = {key: value[-1] if isinstance(value, list) else value for key, value in query.items()}
  flat_query.update({'redirect_uri': callback_url, 'state': state, 'scope': scope})
  return urlunsplit(url_parts._replace(query=urlencode(flat_query)))

class _CallbackServer:
  def __init__(self, host: str, port: int, expected_state: str):
    self.code: str | None = None
    self.error: str | None = None
    self.expected_state = expected_state
    self.done = threading.Event()
    outer = self

    class Handler(BaseHTTPRequestHandler):
      def log_message(self, format, *args):
        return

      def do_GET(self):
        params = parse_qs(urlsplit(self.path).query)
        state = params.get('state', [''])[0]
        if state != outer.expected_state:
          outer.error = 'state mismatch'
          self.send_response(400)
          self.end_headers()
          self.wfile.write(b'NanoShare CLI login failed: state mismatch')
          outer.done.set()
          return
        outer.code = params.get('code', [''])[0]
        outer.error = params.get('error', [''])[0] or None
        self.send_response(200 if outer.code else 400)
        self.end_headers()
        if outer.code:
          self.wfile.write(b'NanoShare CLI login complete. You can close this browser tab.')
        else:
          self.wfile.write(b'NanoShare CLI login failed.')
        outer.done.set()

    self.httpd = ThreadingHTTPServer((host, port), Handler)
    self.url = f'http://{host}:{self.httpd.server_port}/callback'
    self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
    self.started = False

  def start(self) -> None:
    if not self.started:
      self.thread.start()
      self.started = True

  def wait_for_code(self, timeout: float) -> str:
    self.start()
    received_callback = self.done.wait(timeout)
    self.httpd.shutdown()
    self.thread.join(5)
    if self.error:
      raise RuntimeError(self.error)
    if not received_callback or not self.code:
      raise TimeoutError('login timed out')
    return self.code

def login(args) -> int:
  state = secrets.token_urlsafe(24)
  server = _CallbackServer(args.callback_host, args.callback_port, state)
  server.start()
  authorize_url = _make_authorize_url(args.base_url, server.url, state, args.scope)

  print(f'Opening browser for NanoShare login: {authorize_url}', flush=True)
  if not args.no_browser:
    webbrowser.open(authorize_url)
  else:
    print(authorize_url, flush=True)

  try:
    code = server.wait_for_code(args.login_timeout)
    response = httpx.post(
      f'{args.base_url.rstrip("/")}/api/cli/token',
      json={'code': code, 'state': state},
      timeout=args.timeout,
      headers={'Accept': 'application/json'},
    )
    response.raise_for_status()
    data = response.json()
    refresh_token = data.get('refresh_token')
    if not refresh_token:
      raise RuntimeError('server returned no refresh_token')
    config_path = args.config or DEFAULT_CONFIG
    write_config(
      config_path,
      node=args.node,
      url=args.base_url,
      refresh_token=refresh_token,
      token_url=f'{args.base_url.rstrip("/")}/token/refresh',
      source=args.source,
    )
  except Exception as exc:
    print(f'login failed: {exc}')
    return 1

  print(f'Logged in. Config written to {config_path}')
  return 0
