/**
 * `useDeployment` — view-model for `Settings → Deployment` (ticket #68).
 *
 * Owns the draft exposure config, the backend-derived preview, and the embedded
 * server lifecycle. All behaviour lives here so `DeploymentSettings` stays
 * presentation-only (no `invoke()`, no network reasoning in JSX).
 *
 * **The backend owns every network fact.** LAN candidates and the proxy upstream
 * URL are read from `previewExposure`; nothing here derives an address.
 *
 * Two backend behaviours shape this hook:
 *
 * 1. `preview_server_exposure_settings` *validates* before previewing, so an
 *    incomplete draft is rejected rather than previewed. That makes it the
 *    UI's validation authority (it returns the same message `start` would), but
 *    it also means a `remoteProxyOtherMachine` draft cannot be previewed until
 *    it already carries a LAN bind address — which is what the preview is for.
 *    So the LAN candidate list is fetched with a separate always-valid
 *    `localOnly` probe; the backend builds that list independently of the mode.
 * 2. Only `start` reports a runtime failure, so a rejected save/start surfaces
 *    the backend message verbatim — it is the concrete correction to apply.
 */

import { useCallback, useEffect, useRef, useState } from "react";

import type {
  DiagnosticWarning,
  EmbeddedServerStatus,
  GatewayError,
  ServerExposureMode,
  ServerExposureSettings,
} from "@/domain";
import { useGateways } from "@/app/di";

/** Debounce before previewing a draft, so typing doesn't spam the backend. */
const PREVIEW_DEBOUNCE_MS = 250;

function messageOf(e: unknown): string {
  return e && typeof e === "object" && "message" in e
    ? String((e as GatewayError).message)
    : String(e);
}

export interface DeploymentVm {
  /** False until the persisted settings have loaded. */
  ready: boolean;
  /** The draft config being edited (persisted config until the user edits). */
  settings: ServerExposureSettings | null;
  /** Current server status. */
  status: EmbeddedServerStatus;
  /** LAN addresses discovered by the backend (never derived client-side). */
  candidateLanAddresses: string[];
  /** Backend-built upstream URL for the current draft; absent when invalid. */
  upstreamUrl?: string;
  /** Non-fatal diagnostics for the draft — informational, never blocking. */
  warnings: DiagnosticWarning[];
  /** Why the draft is rejected, as told by the backend. Actionable, not decorative. */
  validationError: string | null;
  /** Last save/start/stop failure. */
  actionError: string | null;
  /** True while a start/stop is in flight. */
  busy: boolean;
  setMode: (mode: ServerExposureMode) => void;
  setPublicOrigin: (origin: string) => void;
  setLanBindAddress: (address: string) => void;
  setTrustedProxies: (raw: string) => void;
  setPort: (port: number) => void;
  /** Persists the draft, then starts the server so what runs is what is shown. */
  start: () => Promise<void>;
  stop: () => Promise<void>;
}

export function useDeployment(): DeploymentVm {
  const { desktopServer } = useGateways();
  const [settings, setSettings] = useState<ServerExposureSettings | null>(null);
  const [status, setStatus] = useState<EmbeddedServerStatus>({
    state: "stopped",
  });
  const [candidateLanAddresses, setCandidates] = useState<string[]>([]);
  const [upstreamUrl, setUpstreamUrl] = useState<string | undefined>();
  const [warnings, setWarnings] = useState<DiagnosticWarning[]>([]);
  const [validationError, setValidationError] = useState<string | null>(null);
  const [actionError, setActionError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  // Guards against a stale in-flight preview overwriting a newer one.
  const previewSeq = useRef(0);

  useEffect(() => {
    let alive = true;
    void (async () => {
      try {
        const [persisted, current] = await Promise.all([
          desktopServer.getExposureSettings(),
          desktopServer.status(),
        ]);
        if (!alive) return;
        setSettings(persisted);
        setStatus(current);
      } catch (e) {
        if (alive) setActionError(messageOf(e));
      }
    })();
    return () => {
      alive = false;
    };
  }, [desktopServer]);

  // LAN candidates via an always-valid `localOnly` probe (see the header note).
  useEffect(() => {
    let alive = true;
    void (async () => {
      try {
        const preview = await desktopServer.previewExposure({
          mode: "localOnly",
          port: 0,
          trustedProxies: [],
        });
        if (alive) setCandidates(preview.candidateLanAddresses);
      } catch {
        // No candidates ⇒ the LAN field falls back to free text; the backend
        // still rejects a bad address on save.
      }
    })();
    return () => {
      alive = false;
    };
  }, [desktopServer]);

  useEffect(() => {
    let unsubscribe: (() => void) | undefined;
    let cancelled = false;
    void desktopServer.onStatusChanged(setStatus).then((u) => {
      // The effect may have torn down while the subscription was resolving.
      if (cancelled) u();
      else unsubscribe = u;
    });
    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [desktopServer]);

  // Preview (and thereby validate) the draft, debounced.
  useEffect(() => {
    if (!settings) return;
    const seq = ++previewSeq.current;
    const timer = setTimeout(() => {
      void desktopServer.previewExposure(settings).then(
        (preview) => {
          if (seq !== previewSeq.current) return;
          setUpstreamUrl(preview.upstreamUrl);
          setWarnings(preview.warnings);
          setValidationError(null);
        },
        (e) => {
          if (seq !== previewSeq.current) return;
          // The draft is incomplete/invalid: the backend message *is* the fix.
          setUpstreamUrl(undefined);
          setWarnings([]);
          setValidationError(messageOf(e));
        },
      );
    }, PREVIEW_DEBOUNCE_MS);
    return () => clearTimeout(timer);
  }, [desktopServer, settings]);

  const patch = useCallback((change: Partial<ServerExposureSettings>) => {
    setActionError(null);
    setSettings((prev) => (prev ? { ...prev, ...change } : prev));
  }, []);

  const setMode = useCallback(
    (mode: ServerExposureMode) => {
      // Dropping to a narrower mode clears the fields that mode does not use, so
      // a stale origin/proxy can never be persisted behind the user's back.
      if (mode === "localOnly") {
        patch({ mode, publicOrigin: undefined, lanBindAddress: undefined, trustedProxies: [] });
      } else if (mode === "remoteProxyLocal") {
        patch({ mode, lanBindAddress: undefined, trustedProxies: [] });
      } else {
        patch({ mode });
      }
    },
    [patch],
  );

  const setPublicOrigin = useCallback(
    (origin: string) => patch({ publicOrigin: origin.trim() || undefined }),
    [patch],
  );
  const setLanBindAddress = useCallback(
    (address: string) => patch({ lanBindAddress: address || undefined }),
    [patch],
  );
  const setTrustedProxies = useCallback(
    (raw: string) =>
      patch({
        trustedProxies: raw
          .split(/[\s,]+/)
          .map((entry) => entry.trim())
          .filter(Boolean),
      }),
    [patch],
  );
  const setPort = useCallback((port: number) => patch({ port }), [patch]);

  const start = useCallback(async () => {
    if (!settings) return;
    setBusy(true);
    setActionError(null);
    try {
      // `start` runs the *persisted* config, so persist the draft first —
      // otherwise the server would run something other than what is on screen.
      await desktopServer.saveExposureSettings(settings);
      setStatus(await desktopServer.start());
    } catch (e) {
      setActionError(messageOf(e));
    } finally {
      setBusy(false);
    }
  }, [desktopServer, settings]);

  const stop = useCallback(async () => {
    setBusy(true);
    setActionError(null);
    try {
      setStatus(await desktopServer.stop());
    } catch (e) {
      setActionError(messageOf(e));
    } finally {
      setBusy(false);
    }
  }, [desktopServer]);

  return {
    ready: settings !== null,
    settings,
    status,
    candidateLanAddresses,
    upstreamUrl,
    warnings,
    validationError,
    actionError,
    busy,
    setMode,
    setPublicOrigin,
    setLanBindAddress,
    setTrustedProxies,
    setPort,
    start,
    stop,
  };
}
