from __future__ import annotations

import sys
import threading
import time
from pathlib import Path
from typing import Callable

import httpx

from .client import events_url
from .ignore import is_ignored, load_ignore_patterns
from .sync import relative, state_path, SYNC_DIR_NAME

SyncFunc = Callable[[object], int]

class _ChangeFlag:
  def __init__(self) -> None:
    self.event = threading.Event()

  def notify(self) -> None:
    self.event.set()

  def wait(self, timeout: float) -> bool:
    return self.event.wait(timeout)

  def clear(self) -> None:
    self.event.clear()

def _is_relevant_path(root: Path, path: str, ignore_patterns: list[str]) -> bool:
  try:
    candidate = Path(path).expanduser().resolve()
    rel = relative(root, candidate)
  except ValueError:
    return False
  if candidate == state_path(root) or root / SYNC_DIR_NAME in candidate.parents:
    return False
  return not is_ignored(rel, ignore_patterns)

def _remote_events_thread(args, flag: _ChangeFlag, stop: threading.Event) -> threading.Thread:
  def run() -> None:
    while not stop.is_set():
      client = None
      try:
        from .app import make_client
        client = make_client(args)
        headers = {'Accept': 'text/event-stream', **client.auth_headers()}
        with httpx.Client(timeout=None) as http:
          with http.stream('GET', events_url(client, args.node), headers=headers) as response:
            response.raise_for_status()
            for line in response.iter_lines():
              if stop.is_set():
                return
              if line.startswith('event: files.changed'):
                flag.notify()
      except Exception as exc:
        if not stop.is_set():
          print(f'remote event stream disconnected; retrying in 30s: {exc}', file=sys.stderr)
          stop.wait(30)
      finally:
        if client:
          client.close()

  thread = threading.Thread(target=run, daemon=True)
  thread.start()
  return thread

def _watchdog_observer(root: Path, ignore_patterns: list[str], flag: _ChangeFlag):
  from watchdog.events import FileSystemEventHandler
  from watchdog.observers import Observer

  class Handler(FileSystemEventHandler):
    def on_any_event(self, event):
      paths = [event.src_path]
      dest_path = getattr(event, 'dest_path', None)
      if dest_path:
        paths.append(dest_path)
      if any(_is_relevant_path(root, path, ignore_patterns) for path in paths):
        flag.notify()

  observer = Observer()
  observer.schedule(Handler(), str(root), recursive=True)
  observer.start()
  return observer

def watch_loop(args, sync_func: SyncFunc) -> int:
  root = Path(args.folder).expanduser().resolve()
  root.mkdir(parents=True, exist_ok=True)
  ignore_patterns = load_ignore_patterns(root, getattr(args, 'ignore', None))
  interval = max(float(getattr(args, 'interval', 10.0)), 0.1)
  flag = _ChangeFlag()
  observer = None
  stop_events = threading.Event()
  events_thread = None

  try:
    try:
      observer = _watchdog_observer(root, ignore_patterns, flag)
      print(f'watching {args.folder} for local changes; remote events with {interval}s fallback')
    except Exception as exc:
      print(f'watchdog unavailable, polling every {interval}s: {exc}', file=sys.stderr)
      print(f'watching {args.folder} every {interval}s')

    events_thread = _remote_events_thread(args, flag, stop_events)

    while True:
      flag.clear()
      code = sync_func(args)
      if code:
        print(f'watch sync failed; retrying in {interval}s', file=sys.stderr)
        flag.wait(interval)
        continue

      if flag.wait(interval):
        flag.clear()
        time.sleep(1.0)
        while flag.wait(1.0):
          flag.clear()
  except KeyboardInterrupt:
    return 0
  finally:
    stop_events.set()
    if observer:
      observer.stop()
      observer.join(timeout=5)
    if events_thread:
      events_thread.join(timeout=5)
