import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api, bootstrap } from '@/api/client';
import type {
  AppBadgeInfo,
  DataContextGroup,
  FieldOption,
  TestRunResult,
  IntegrationSummary,
  NodeSummary,
  TemplateSummary,
  ValidationReport,
  Workflow,
  WorkflowGraph,
  WorkflowSummary,
} from '@/types';

/**
 * Server state lives here; nothing else calls the API directly.
 *
 * Query keys are namespaced so invalidating one workflow does not refetch the
 * whole list, and the list does not go stale when a graph is saved.
 */

export const keys = {
  nodes: ['flowwp', 'nodes'] as const,
  integrations: ['flowwp', 'integrations'] as const,
  workflows: ['flowwp', 'workflows'] as const,
  templates: ['flowwp', 'templates'] as const,
  workflow: (id: number) => ['flowwp', 'workflow', id] as const,
  executions: (workflow: number | null, status: string, page: number) =>
    ['flowwp', 'executions', workflow, status, page] as const,
  execution: (id: number) => ['flowwp', 'execution', id] as const,
  options: (source: string, search: string, parent: string) =>
    ['flowwp', 'options', source, search, parent] as const,
};

/** The node catalogue — server-authored, so it is cached hard. */
export function useNodeCatalogue() {
  return useQuery({
    queryKey: keys.nodes,
    queryFn: async () => (await api.get<NodeSummary[]>('/nodes')).data,
    // Node definitions only change when a plugin is activated, which reloads
    // the page anyway. Refetching them per navigation is pure waste.
    staleTime: Infinity,
  });
}

/** Integration groups for the node library. Cached as hard as the catalogue. */
export function useIntegrations() {
  return useQuery({
    queryKey: keys.integrations,
    queryFn: async () => (await api.get<IntegrationSummary[]>('/integrations')).data,
    staleTime: Infinity,
  });
}

interface WorkflowListResponse {
  data: WorkflowSummary[];
  meta?: { total?: number };
  integrations?: Record<string, AppBadgeInfo>;
}

export type WorkflowListData = {
  items: WorkflowSummary[];
  total: number;
  apps: Record<string, AppBadgeInfo>;
};

export function useWorkflows() {
  return useQuery({
    queryKey: keys.workflows,
    queryFn: async (): Promise<WorkflowListData> => {
      // The whole list in one page: the filters above the table work on what
      // was fetched, and a site with more than a hundred workflows is rare.
      const response = (await api.get<WorkflowSummary[]>('/workflows?per_page=100')) as WorkflowListResponse;

      return {
        items: response.data,
        total: response.meta?.total ?? response.data.length,
        apps: response.integrations ?? {},
      };
    },
  });
}

export function useWorkflow(id: number | null) {
  return useQuery({
    queryKey: keys.workflow(id ?? 0),
    queryFn: async () => (await api.get<Workflow>(`/workflows/${id}`)).data,
    enabled: id !== null,
  });
}

export function useCreateWorkflow() {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async (name: string) => (await api.post<Workflow>('/workflows', { name })).data,
    onSuccess: () => {
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

/** The template gallery. Fetched when the gallery opens, not with the list. */
export function useTemplates(enabled: boolean) {
  return useQuery({
    queryKey: keys.templates,
    queryFn: async () => (await api.get<TemplateSummary[]>('/templates')).data,
    enabled,
    staleTime: 60_000,
  });
}

/** Start a workflow from a template; answers with the new workflow's id. */
export function useCreateFromTemplate() {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async (id: string) =>
      (await api.post<{ id: number; name: string; needs: string[] }>(`/templates/${encodeURIComponent(id)}/use`, {}))
        .data,
    onSuccess: () => {
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

export function useDeleteWorkflow() {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async (id: number) => api.delete<{ deleted: boolean }>(`/workflows/${id}`),
    onSuccess: () => {
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

/**
 * Rename without touching the graph.
 *
 * Deliberately not a call to useSaveWorkflow with a name: sending the graph
 * would store a new version, so correcting a typo in a title would show up in
 * the history as an edit to the workflow itself.
 */
export function useRenameWorkflow() {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, name }: { id: number; name: string }) =>
      (await api.put<Workflow>(`/workflows/${id}`, { name })).data,
    onSuccess: (workflow) => {
      client.setQueryData(keys.workflow(workflow.id), workflow);
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

export interface SaveResult {
  workflow: Workflow;
  validation: ValidationReport | null;
}

export function useSaveWorkflow(id: number) {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async (payload: { graph: WorkflowGraph; name?: string }): Promise<SaveResult> => {
      const response = await api.put<Workflow>(`/workflows/${id}`, payload);

      return {
        workflow: response.data,
        validation: (response.meta?.validation as ValidationReport | undefined) ?? null,
      };
    },
    onSuccess: (result) => {
      // Seed the cache from the response rather than refetching: the server
      // just told us the canonical state.
      client.setQueryData(keys.workflow(id), result.workflow);
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

export function useValidateWorkflow(id: number) {
  return useMutation({
    mutationFn: async (payload: { graph: WorkflowGraph; activating?: boolean }) =>
      (await api.post<ValidationReport>(`/workflows/${id}/validate`, payload)).data,
  });
}

export function useActivateWorkflow(id: number) {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async () => (await api.post<Workflow>(`/workflows/${id}/activate`)).data,
    onSuccess: (workflow) => {
      client.setQueryData(keys.workflow(id), workflow);
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

export function useDeactivateWorkflow(id: number) {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async () => (await api.post<Workflow>(`/workflows/${id}/deactivate`)).data,
    onSuccess: (workflow) => {
      client.setQueryData(keys.workflow(id), workflow);
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

/**
 * The switch on the workflow list: take a workflow live, or pause it.
 *
 * Optimistic, because a switch that waits for the server before moving feels
 * broken. Going live validates the whole graph, so it can be refused; the row
 * snaps back and the caller shows why.
 */
export function useSetWorkflowActive() {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, active }: { id: number; active: boolean }) =>
      (await api.post<Workflow>(`/workflows/${id}/${active ? 'activate' : 'deactivate'}`)).data,
    onMutate: async ({ id, active }) => {
      await client.cancelQueries({ queryKey: keys.workflows });

      const previous = client.getQueryData<WorkflowListData>(keys.workflows);

      if (previous) {
        client.setQueryData<WorkflowListData>(keys.workflows, {
          ...previous,
          items: previous.items.map((item) =>
            item.id === id ? { ...item, isActive: active, status: active ? 'active' : 'inactive' } : item,
          ),
        });
      }

      return { previous };
    },
    onError: (_error, _variables, context) => {
      if (context?.previous) {
        client.setQueryData(keys.workflows, context.previous);
      }
    },
    onSuccess: (workflow) => {
      client.setQueryData(keys.workflow(workflow.id), workflow);
    },
    onSettled: () => {
      void client.invalidateQueries({ queryKey: keys.workflows });
    },
  });
}

/** What data the given node can reach, for the token picker. */
export function useDataContext(id: number) {
  return useMutation({
    mutationFn: async (payload: { graph: WorkflowGraph; node: string }) =>
      (await api.post<DataContextGroup[]>(`/workflows/${id}/data-context`, payload)).data,
  });
}

/** Run the workflow with side effects simulated. */
export function useTestRun(id: number) {
  const client = useQueryClient();

  return useMutation({
    mutationFn: async (payload?: Record<string, unknown>) =>
      (await api.post<TestRunResult>(`/workflows/${id}/test`, { payload: payload ?? {} })).data,
    onSuccess: () => {
      // A test run writes execution rows, so anything showing run history is
      // now stale.
      void client.invalidateQueries({ queryKey: keys.workflow(id) });
    },
  });
}

/**
 * The choices a config field's dropdown offers, from the live site.
 *
 * Fetched only while a field is open, and cached briefly rather than forever:
 * unlike the node catalogue, products and courses change while the editor is
 * open in another tab.
 */
export function useFieldOptions(source: string, search: string, parent: string, enabled: boolean) {
  return useQuery({
    queryKey: keys.options(source, search, parent),
    queryFn: async () => {
      const params = new URLSearchParams();

      if (search !== '') {
        params.set('search', search);
      }

      if (parent !== '') {
        params.set('parent', parent);
      }

      const query = params.toString();
      // With plain permalinks the REST root is `?rest_route=`, so a query
      // string has to continue it rather than start one.
      const joiner = bootstrap().apiRoot.includes('?') ? '&' : '?';
      const path = `/options/${encodeURIComponent(source)}${query === '' ? '' : joiner + query}`;

      return (await api.get<FieldOption[]>(path)).data;
    },
    enabled,
    staleTime: 60_000,
    // Keep the last list on screen while the next search loads, instead of
    // flashing "Loading…" on every keystroke.
    placeholderData: (previous) => previous,
  });
}
