import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';

// The data layer for a SINGLE-connection sidecar — one where the owner has exactly one of the thing, so the
// whole configuration is one row and one form. Transmission and slskd both work this way; the registries
// (photos, invoiceshelf) do not, and deliberately have their own hooks.
//
// Every such sidecar serves the same three-verb contract at `/<service>/_config`, so the hook is written
// once here rather than copied per app:
//
//   GET    → { configured, connection }   never any secret, only whether one is stored
//   PUT    → { connection }               upsert, validated against the live service first
//   DELETE → { configured: false }        forget it
//
// plus `/<service>/_health`, whose failure BODIES are the useful part — see useServiceHealth.

export type ServiceConnection = {
  id: number;
  service: string;
  /**
   * Null means "inherit the instance" — the row is a bare credential and the base URL comes from the
   * owner's row. Only multi-user services (gitea) ever store null; a single-daemon service always sets it.
   * Mirror of officerdb's `url: text('url')`, which is nullable since the one-instance-many-tokens change.
   */
  url: string | null;
  username: string | null;
  path: string | null;
  hasSecret: boolean;
  version: string | null;
  lastSeenAt: string | null;
  createdAt: string;
};

export type ServiceConnectionState = { configured: boolean; connection: ServiceConnection | null };

export type ServiceHealth = { ok: boolean; configured: boolean; version?: string | null; error?: string; ms?: number };

/** Unwrap the `{ status, message }` useClient throws, where `message` is the sidecar's JSON body. */
export function serviceErrorMessage(err: unknown): string {
  const raw = (err as { message?: unknown } | null)?.message;
  if (typeof raw !== 'string' || !raw) return 'Something went wrong';
  try {
    const parsed = JSON.parse(raw) as { error?: unknown };
    if (typeof parsed.error === 'string' && parsed.error) return parsed.error;
  } catch {
    /* plain text */
  }
  return raw.slice(0, 300);
}

const configKey = (service: string) => [service, 'connection'] as const;
const healthKey = (service: string) => [service, 'health'] as const;

/**
 * `TState` widens the GET body for services that answer with more than the two common fields — the wallet
 * adds the URL actually in use and whether it is the owner's or a fallback. The base shape is required, so
 * `configured`/`connection` are always there to branch on.
 */
export function useServiceConnection<TState extends ServiceConnectionState = ServiceConnectionState>(service: string) {
  const { get } = useClient();
  return useQuery({
    queryKey: configKey(service),
    queryFn: () => get<TState>(`/${service}/_config`),
    staleTime: 60_000,
    retry: false,
  });
}

/**
 * Health, including its failure bodies.
 *
 * `get` throws on any status >= 400, so a plain query would leave `data` undefined for exactly the two cases
 * the UI most needs to tell apart — 503 not connected and 502 connected-but-broken. Both carry a JSON body,
 * so the throw is turned back into the answer rather than an error state.
 */
export function useServiceHealth(service: string) {
  const { get } = useClient();
  return useQuery({
    queryKey: healthKey(service),
    queryFn: async (): Promise<ServiceHealth> => {
      try {
        return await get<ServiceHealth>(`/${service}/_health`);
      } catch (err) {
        const raw = (err as { message?: unknown } | null)?.message;
        if (typeof raw === 'string') {
          try {
            const body = JSON.parse(raw) as ServiceHealth;
            if (body && body.ok === false) return body;
          } catch {
            /* not the sidecar's body */
          }
        }
        // Anything else — the platform proxy, auth, the sidecar being down — is a configured service that is
        // failing, not an unconfigured one. Never offer the setup form on a guess.
        return { ok: false, configured: true, error: serviceErrorMessage(err) };
      }
    },
    staleTime: 60_000,
    retry: false,
  });
}

/**
 * Save and forget. Both invalidate the WHOLE service prefix, not just the connection: re-pointing at a
 * different daemon invalidates every list, stat and setting already in the cache.
 */
export function useServiceConnectionActions<
  TInput extends Record<string, unknown>,
  TSaved = { connection: ServiceConnection },
>(service: string) {
  const { put, delete: del } = useClient();
  const qc = useQueryClient();

  const invalidate = () => qc.invalidateQueries({ queryKey: [service] });

  const save = useMutation({
    mutationFn: (input: TInput) => put<TSaved>(`/${service}/_config`, input),
    onSuccess: invalidate,
  });

  const forget = useMutation({
    mutationFn: () => del<ServiceConnectionState>(`/${service}/_config`),
    onSuccess: invalidate,
  });

  return { save, forget };
}
