import {
  useInfiniteQuery,
  useMutation,
  useQuery,
  useQueryClient,
  type InfiniteData,
  type QueryClient,
} from '@tanstack/vue-query'
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
import { apiFetch } from '../services/api'
import { createMutationRequestContext } from '../services/mutationContext'
import type { components, paths } from './generated/schema'
import { queryKeys } from './queryClient'
import type {
  OpenClawCollection,
  OpenClawCronJob,
  OpenClawCronJobDetail,
  OpenClawCronRun,
  OpenClawCronRunEnqueue,
  OpenClawOperation,
} from '../types/openclaw'
import { reportOperationEnvelope } from '../services/operationResults'

type GeneratedCreateCronRequest =
  components['schemas']['CreateOpenClawCronJobRequest']
type GeneratedPatchCronRequest =
  components['schemas']['PatchOpenClawCronJobRequest']
type GeneratedCronListQuery = NonNullable<
  paths['/api/v1/openclaw/cron']['get']['parameters']['query']
>
type JsonBody<TResponse> = TResponse extends {
  content: { 'application/json': infer TBody }
}
  ? TBody
  : never
type GeneratedCronCollection = JsonBody<
  paths['/api/v1/openclaw/cron']['get']['responses'][200]
>
type GeneratedCronJob = components['schemas']['OpenClawCronJobDto']
type GeneratedCronJobDetail = components['schemas']['OpenClawCronJobDetailDto']
type GeneratedCronDetailOperation = JsonBody<
  paths['/api/v1/openclaw/cron/{jobId}']['get']['responses'][200]
>

export type OpenClawCronCollectionDto = OpenClawCollection<OpenClawCronJob>
export type OpenClawCronDetailOperationDto = GeneratedCronDetailOperation

export type CreateOpenClawCronJobRequest = Omit<
  GeneratedCreateCronRequest,
  'schedule' | 'payload' | 'delivery' | 'trigger'
> & {
  schedule: Record<string, unknown>
  payload: Record<string, unknown>
  delivery?: Record<string, unknown> | null
  trigger?: Record<string, unknown> | null
}

export type PatchOpenClawCronJobRequest = Omit<
  GeneratedPatchCronRequest,
  'patch'
> & {
  patch: Record<string, unknown>
}

export interface OpenClawCronListOptions {
  includeDisabled?: boolean
  limit?: number
}

interface OpenClawErrorPayload {
  detail?: string
  title?: string
  message?: string
  recovery?: string
  state?: string
}

async function readJson<T>(
  path: string,
  init?: RequestInit,
  fallback = 'OpenClaw-Anfrage ist fehlgeschlagen',
  operationTitle?: string,
): Promise<T> {
  const response = await apiFetch(path, init)
  const payload = await response.json().catch(() => null) as T | OpenClawErrorPayload | null
  if (operationTitle) reportOperationEnvelope(payload, operationTitle)
  if (!response.ok) {
    const failure = payload as OpenClawErrorPayload | null
    const message = failure?.detail
      || failure?.message
      || failure?.title
      || fallback
    throw new Error(failure?.recovery ? `${message} ${failure.recovery}` : message)
  }
  return payload as T
}

function mutationHeaders(operation: string, expectedHash?: string | null): Headers {
  const headers = createMutationRequestContext(operation).headers
  if (expectedHash) headers.set('If-Match', expectedHash)
  return headers
}

function requireOperationData<T>(result: OpenClawOperation<T>): T {
  if (!result.ok || !result.data) {
    throw new Error(result.recovery
      ? `${result.message} ${result.recovery}`
      : result.message)
  }
  return result.data
}

function normalizeListOptions(
  options: OpenClawCronListOptions = {},
): Required<OpenClawCronListOptions> {
  return {
    includeDisabled: options.includeDisabled ?? true,
    limit: Math.min(Math.max(options.limit ?? 50, 1), 200),
  }
}

function nullableNumber(value: number | string | null): number | null {
  if (value === null) return null
  const parsed = Number(value)
  return Number.isFinite(parsed) ? parsed : null
}

function normalizeCronJob(job: GeneratedCronJob): OpenClawCronJob {
  return {
    ...job,
    resourceHash: job.resourceHash ?? null,
  }
}

function normalizeCronDetail(detail: GeneratedCronJobDetail): OpenClawCronJobDetail {
  return {
    ...detail,
    schedule: {
      ...detail.schedule,
      everyMs: nullableNumber(detail.schedule.everyMs),
      anchorMs: nullableNumber(detail.schedule.anchorMs),
      staggerMs: nullableNumber(detail.schedule.staggerMs),
    },
    payload: {
      ...detail.payload,
      timeoutSeconds: nullableNumber(detail.payload.timeoutSeconds),
      noOutputTimeoutSeconds: nullableNumber(detail.payload.noOutputTimeoutSeconds),
      outputMaxBytes: nullableNumber(detail.payload.outputMaxBytes),
    },
    failureAlert: detail.failureAlert
      ? {
          ...detail.failureAlert,
          after: nullableNumber(detail.failureAlert.after),
          cooldownMs: nullableNumber(detail.failureAlert.cooldownMs),
        }
      : null,
  }
}

export async function fetchOpenClawCronJobs(
  options: OpenClawCronListOptions,
  cursor: string | null,
  signal?: AbortSignal,
): Promise<OpenClawCronCollectionDto> {
  const normalized = normalizeListOptions(options)
  const query: GeneratedCronListQuery = {
    includeDisabled: normalized.includeDisabled,
    limit: normalized.limit,
  }
  if (cursor) query.cursor = cursor
  const parameters = new URLSearchParams()
  parameters.set('includeDisabled', String(query.includeDisabled))
  parameters.set('limit', String(query.limit))
  if (query.cursor) parameters.set('cursor', query.cursor)
  const collection = await readJson<GeneratedCronCollection>(
    `/api/v1/openclaw/cron?${parameters}`,
    { signal },
    'OpenClaw-Zeitpläne konnten nicht geladen werden',
  )
  return {
    ...collection,
    state: collection.state as OpenClawCronCollectionDto['state'],
    items: collection.items.map(normalizeCronJob),
  }
}

export async function fetchOpenClawCronDetail(
  jobId: string,
  signal?: AbortSignal,
): Promise<OpenClawCronJobDetail> {
  const result = await readJson<OpenClawCronDetailOperationDto>(
    `/api/v1/openclaw/cron/${encodeURIComponent(jobId)}`,
    { signal },
    'Zeitplandetails konnten nicht geladen werden',
  )
  if (!result.ok || !result.data) {
    throw new Error(result.recovery
      ? `${result.message} ${result.recovery}`
      : result.message)
  }
  return normalizeCronDetail(result.data)
}

export async function fetchOpenClawCronRuns(
  jobId: string,
  runId: string | null,
  cursor: string | null,
  signal?: AbortSignal,
): Promise<OpenClawCollection<OpenClawCronRun>> {
  const parameters = new URLSearchParams({ limit: '25' })
  if (runId) parameters.set('runId', runId)
  if (cursor) parameters.set('cursor', cursor)
  return readJson(
    `/api/v1/openclaw/cron/${encodeURIComponent(jobId)}/runs?${parameters}`,
    { signal },
    'Cron-Verlauf konnte nicht geladen werden',
  )
}

export function mergeOpenClawCronPages<T>(
  data: InfiniteData<OpenClawCollection<T>, string | null> | undefined,
): OpenClawCollection<T> | null {
  if (!data?.pages.length) return null
  const last = data.pages.at(-1)!
  const items = new Map<string, T>()
  for (const page of data.pages) {
    for (const item of page.items) {
      const id = (item as { id?: unknown }).id
      if (typeof id === 'string') items.set(id, item)
    }
  }
  return { ...last, items: [...items.values()] }
}

function toCronSummary(detail: OpenClawCronJobDetail): OpenClawCronJob {
  return {
    id: detail.id,
    name: detail.name,
    description: detail.description,
    schedule: detail.schedule.expression ?? detail.schedule.kind,
    timeZone: detail.schedule.timeZone,
    enabled: detail.enabled,
    status: detail.lastRunStatus ?? 'idle',
    agentId: detail.agentId,
    sessionKey: detail.sessionKey,
    nextRunAt: detail.nextRunAt,
    lastRunAt: detail.lastRunAt,
    lastRunStatus: detail.lastRunStatus,
    lastError: detail.lastError,
    canRun: detail.canRun,
    resourceHash: detail.resourceHash,
  }
}

export function applyOpenClawCronDetail(
  client: QueryClient,
  detail: OpenClawCronJobDetail,
): void {
  const summary = toCronSummary(detail)
  for (const [queryKey, current] of client.getQueriesData<
    InfiniteData<OpenClawCollection<OpenClawCronJob>, string | null>
  >({ queryKey: queryKeys.openClawCron() })) {
    if (!current?.pages || typeof queryKey[2] !== 'object') continue
    const filters = queryKey[2] as OpenClawCronListOptions
    const pages = current.pages.map((page, pageIndex) => {
      const withoutCurrent = page.items.filter(item => item.id !== detail.id)
      const shouldInclude = filters.includeDisabled !== false || detail.enabled
      const items = pageIndex === 0 && shouldInclude
        ? [summary, ...withoutCurrent]
        : withoutCurrent
      return { ...page, items }
    })
    client.setQueryData(queryKey, { ...current, pages })
  }
}

export function removeOpenClawCronFromCache(
  client: QueryClient,
  jobId: string,
): void {
  for (const [queryKey, current] of client.getQueriesData<
    InfiniteData<OpenClawCollection<OpenClawCronJob>, string | null>
  >({ queryKey: queryKeys.openClawCron() })) {
    if (!current?.pages) continue
    client.setQueryData(queryKey, {
      ...current,
      pages: current.pages.map(page => ({
        ...page,
        items: page.items.filter(item => item.id !== jobId),
      })),
    })
  }
  client.removeQueries({ queryKey: queryKeys.openClawCronDetail(jobId) })
  client.removeQueries({ queryKey: ['openclaw', 'cron', 'runs', jobId] })
}

async function createCronJob(
  request: CreateOpenClawCronJobRequest,
): Promise<OpenClawOperation<OpenClawCronJobDetail>> {
  const result = await readJson<OpenClawOperation<OpenClawCronJobDetail>>('/api/v1/openclaw/cron', {
    method: 'POST',
    headers: mutationHeaders('openclaw-cron-create'),
    body: JSON.stringify(request),
  }, 'Zeitplan konnte nicht erstellt werden', 'Cronjob erstellt')
  if (!result.ok || (!result.data && result.state !== 'replayed')) {
    requireOperationData(result)
  }
  return result
}

async function patchCronJob(input: {
  jobId: string
  patch: Record<string, unknown>
  expectedHash: string
}): Promise<OpenClawOperation<OpenClawCronJobDetail>> {
  if (!input.expectedHash.trim()) throw new Error('A current OpenClaw resource hash is required.')
  const body: PatchOpenClawCronJobRequest = {
    patch: input.patch,
    expectedHash: input.expectedHash,
  }
  const result = await readJson<OpenClawOperation<OpenClawCronJobDetail>>(
    `/api/v1/openclaw/cron/${encodeURIComponent(input.jobId)}`, {
    method: 'PATCH',
    headers: mutationHeaders('openclaw-cron-update', input.expectedHash),
    body: JSON.stringify(body),
  }, 'Zeitplan konnte nicht aktualisiert werden', 'Cronjob aktualisiert')
  if (!result.ok || (!result.data && result.state !== 'replayed')) {
    requireOperationData(result)
  }
  return result
}

async function deleteCronJob(input: {
  jobId: string
  expectedHash: string
}): Promise<OpenClawOperation<Record<string, unknown>>> {
  if (!input.expectedHash.trim()) throw new Error('A current OpenClaw resource hash is required.')
  const result = await readJson<OpenClawOperation<Record<string, unknown>>>(
    `/api/v1/openclaw/cron/${encodeURIComponent(input.jobId)}?expectedHash=${encodeURIComponent(input.expectedHash)}`,
    {
      method: 'DELETE',
      headers: mutationHeaders('openclaw-cron-delete', input.expectedHash),
    },
    'Zeitplan konnte nicht gelöscht werden',
    'Cronjob gelöscht',
  )
  if (!result.ok) requireOperationData(result)
  return result
}

async function runCronJob(input: {
  jobId: string
  expectedHash: string
}): Promise<OpenClawOperation<OpenClawCronRunEnqueue>> {
  if (!input.expectedHash.trim()) throw new Error('A current OpenClaw resource hash is required.')
  const result = await readJson<OpenClawOperation<OpenClawCronRunEnqueue>>(
    `/api/v1/openclaw/cron/${encodeURIComponent(input.jobId)}/run?expectedHash=${encodeURIComponent(input.expectedHash)}`,
    {
      method: 'POST',
      headers: mutationHeaders('openclaw-cron-run', input.expectedHash),
    },
    'Cron-Run konnte nicht eingereiht werden',
    'Cronjob gestartet',
  )
  if (!result.ok || (!result.data && result.state !== 'replayed')) {
    requireOperationData(result)
  }
  if (result.data && !result.data.enqueued) {
    throw new Error(result.recovery
      ? `${result.message} ${result.recovery}`
      : result.message)
  }
  return result
}

export function useOpenClawCronJobs(
  options: MaybeRefOrGetter<OpenClawCronListOptions>,
) {
  const normalized = computed(() => normalizeListOptions(toValue(options)))
  const query = useInfiniteQuery({
    queryKey: computed(() => queryKeys.openClawCron(normalized.value)),
    queryFn: ({ pageParam, signal }) =>
      fetchOpenClawCronJobs(normalized.value, pageParam, signal),
    initialPageParam: null as string | null,
    getNextPageParam: lastPage => lastPage.nextCursor ?? undefined,
    placeholderData: previous => previous,
  })
  const collection = computed(() => mergeOpenClawCronPages(
    query.data.value as InfiniteData<OpenClawCronCollectionDto, string | null> | undefined,
  ))
  return { query, collection }
}

export function useOpenClawCronDetail(
  jobId: MaybeRefOrGetter<string>,
  enabled: MaybeRefOrGetter<boolean>,
) {
  const resolvedId = computed(() => toValue(jobId))
  return useQuery({
    queryKey: computed(() => queryKeys.openClawCronDetail(resolvedId.value)),
    queryFn: ({ signal }) => fetchOpenClawCronDetail(resolvedId.value, signal),
    enabled: computed(() => toValue(enabled) && resolvedId.value.length > 0),
    placeholderData: (previous, previousQuery) =>
      previousQuery?.queryKey.at(-1) === resolvedId.value ? previous : undefined,
  })
}

export function useOpenClawCronRuns(
  jobId: MaybeRefOrGetter<string>,
  runId: MaybeRefOrGetter<string | null>,
  enabled: MaybeRefOrGetter<boolean>,
) {
  const resolvedJobId = computed(() => toValue(jobId))
  const resolvedRunId = computed(() => toValue(runId))
  const query = useInfiniteQuery({
    queryKey: computed(() =>
      queryKeys.openClawCronRuns(resolvedJobId.value, resolvedRunId.value),
    ),
    queryFn: ({ pageParam, signal }) =>
      fetchOpenClawCronRuns(
        resolvedJobId.value,
        resolvedRunId.value,
        pageParam,
        signal,
      ),
    initialPageParam: null as string | null,
    getNextPageParam: lastPage => lastPage.nextCursor ?? undefined,
    enabled: computed(() => toValue(enabled) && resolvedJobId.value.length > 0),
    placeholderData: (previous, previousQuery) =>
      previousQuery?.queryKey[3] === resolvedJobId.value
        && (previousQuery.queryKey[4] as { runId?: string | null })?.runId === resolvedRunId.value
        ? previous
        : undefined,
  })
  const collection = computed(() => mergeOpenClawCronPages(
    query.data.value as InfiniteData<
      OpenClawCollection<OpenClawCronRun>,
      string | null
    > | undefined,
  ))
  return { query, collection }
}

export function useOpenClawCronMutations() {
  const client = useQueryClient()
  const refreshLists = () =>
    client.invalidateQueries({
      predicate: query => {
        const key = query.queryKey
        return key[0] === 'openclaw'
          && key[1] === 'cron'
          && (
            key.length === 2
            || (typeof key[2] === 'object' && key[2] !== null)
          )
      },
    })

  const createMutation = useMutation({
    mutationFn: createCronJob,
    onSuccess: result => {
      if (result.data) {
        client.setQueryData(queryKeys.openClawCronDetail(result.data.id), result.data)
        applyOpenClawCronDetail(client, result.data)
      }
      void refreshLists()
    },
  })
  const patchMutation = useMutation({
    mutationFn: patchCronJob,
    onSuccess: (result, input) => {
      if (result.data) {
        client.setQueryData(queryKeys.openClawCronDetail(input.jobId), result.data)
        applyOpenClawCronDetail(client, result.data)
      }
      void client.invalidateQueries({
        queryKey: queryKeys.openClawCronDetail(input.jobId),
      })
      void refreshLists()
    },
  })
  const deleteMutation = useMutation({
    mutationFn: deleteCronJob,
    onSuccess: (result, input) => {
      removeOpenClawCronFromCache(client, input.jobId)
      void refreshLists()
    },
  })
  const runMutation = useMutation({
    mutationFn: runCronJob,
    onSuccess: (result, input) => {
      void client.invalidateQueries({
        queryKey: ['openclaw', 'cron', 'runs', input.jobId],
      })
      void refreshLists()
    },
  })

  return {
    createMutation,
    patchMutation,
    deleteMutation,
    runMutation,
  }
}
