import { create } from 'zustand';
import axios, { AxiosRequestConfig } from 'axios';

/**
 * Offline POST/PUT/DELETE queue.
 *
 * Persists in localStorage under `offline-queue`. Capped at MAX_ENTRIES (the
 * oldest entry is dropped on overflow with a console.warn). Flushed in FIFO
 * order when the browser comes back online or when the user clicks
 * "Retry now" on the queue indicator.
 *
 * The queued HTTP call is replayed by constructing a fresh axios instance
 * with the same method / url / data / headers. The 7-day JWT in
 * localStorage is re-attached at flush time so the replayed request still
 * authenticates. 4xx responses (client errors) are dropped from the queue
 * with a console.warn — they will not succeed on retry. 5xx and network
 * errors keep the entry in the queue for the next flush.
 *
 * This queue is the *only* path for unknown POST/PUT/DELETE writes to the
 * local DB now that B.2 has stripped the generic `INSERT INTO ${tableName}`
 * fallback in api.ts. It is intentionally separate from the
 * `DeviceSyncEngine` in `lib/sync.ts`, which pushes server-owned table
 * rows (attendance, payments, etc.) to /api/sync/push on a 30s interval.
 * The queue here is for client-issued HTTP writes that the server should
 * process in real time when it returns.
 */

const STORAGE_KEY = 'offline-queue';
export const MAX_ENTRIES = 100;

export type QueuedMethod = 'POST' | 'PUT' | 'DELETE' | 'PATCH';

export interface QueuedRequest {
  uid: string;          // uuid for the queue entry itself (distinct from the body's uid)
  method: QueuedMethod;
  url: string;          // absolute path beginning with '/' (e.g. '/api/finance/expenses')
  data: any;            // body
  headers: Record<string, string>;
  createdAt: number;    // epoch ms
  attempts: number;     // # of times flush has tried this entry
  lastError?: string;   // last error message (4xx drops the entry; network/5xx keeps it)
}

interface OfflineQueueState {
  items: QueuedRequest[];
  length: number;
  /** Add a new request to the tail of the queue. Drops the head on overflow. */
  push: (req: Omit<QueuedRequest, 'uid' | 'createdAt' | 'attempts'>) => void;
  /**
   * Replay every queued request in FIFO order. Stops on the first 4xx
   * (which drops that entry and surfaces the error) and on the first 5xx
   * / network error (which keeps the entry and yields back to the caller).
   * Returns the number of entries that were successfully replayed.
   */
  flush: () => Promise<number>;
  /** Drop every entry. Used by the "Discard queue" affordance. */
  clear: () => void;
  /** Re-read from localStorage. Used by main.tsx on app boot. */
  hydrate: () => void;
}

const readStorage = (): QueuedRequest[] => {
  if (typeof localStorage === 'undefined') return [];
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed.filter(
      (r) =>
        r &&
        typeof r.uid === 'string' &&
        typeof r.method === 'string' &&
        typeof r.url === 'string'
    );
  } catch (e) {
    console.warn('[offlineQueue] failed to read storage', e);
    return [];
  }
};

const writeStorage = (items: QueuedRequest[]): void => {
  if (typeof localStorage === 'undefined') return;
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
  } catch (e) {
    console.warn('[offlineQueue] failed to write storage', e);
  }
};

const newUid = (): string => {
  // crypto.randomUUID is available in all modern browsers and inside the
  // Vite dev server; fall back to a Math.random + Date.now blend if not.
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID();
  }
  return `q-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
};

export const useOfflineQueue = create<OfflineQueueState>((set, get) => ({
  items: [],
  length: 0,

  push: (req) => {
    const entry: QueuedRequest = {
      uid: newUid(),
      method: req.method,
      url: req.url,
      data: req.data,
      headers: req.headers || {},
      createdAt: Date.now(),
      attempts: 0,
    };
    const next = [...get().items, entry];
    let dropped: QueuedRequest | undefined;
    if (next.length > MAX_ENTRIES) {
      dropped = next.shift();
      console.warn(
        `[offlineQueue] cap ${MAX_ENTRIES} exceeded; dropped oldest entry`,
        dropped
      );
    }
    writeStorage(next);
    set({ items: next, length: next.length });
  },

  flush: async () => {
    const items = get().items;
    if (items.length === 0) return 0;

    // Replay uses a dedicated axios instance (not the shared `api` in
    // store/api.ts) so the response interceptor doesn't re-enter the
    // offline fallback loop. Direct hits to localhost:3001.
    const replay = axios.create({ baseURL: '/api', timeout: 15000 });
    const authToken = (() => {
      try {
        const raw = localStorage.getItem('auth-storage');
        if (!raw) return null;
        return JSON.parse(raw)?.state?.token || null;
      } catch {
        return null;
      }
    })();

    let successCount = 0;
    const remaining: QueuedRequest[] = [];
    const updated = [...items]; // work in a copy; we re-write at the end

    for (const entry of updated) {
      try {
        const config: AxiosRequestConfig = {
          method: entry.method.toLowerCase() as any,
          url: entry.url.replace(/^\/api/, ''), // baseURL is /api
          data: entry.data,
          headers: {
            ...entry.headers,
            ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
          },
          // Important: do NOT retry forever. The replay axios has no
          // offline interceptor, so a network error bubbles up and we
          // keep the entry for the next flush.
        };
        await replay.request(config);
        successCount += 1;
        // Successful replay — entry is consumed; do not push to `remaining`.
      } catch (err: any) {
        const status = err?.response?.status;
        const isClientError = typeof status === 'number' && status >= 400 && status < 500;
        entry.attempts += 1;
        entry.lastError =
          err?.response?.data?.error ||
          err?.response?.data?.message ||
          err?.message ||
          'Unknown error';
        if (isClientError) {
          // 4xx is permanent — do not retry. Drop the entry and surface.
          console.warn(
            `[offlineQueue] dropping ${entry.method} ${entry.url} — ${status} ${entry.lastError}`
          );
          continue;
        }
        // 5xx or network error — keep for the next flush.
        remaining.push(entry);
      }
    }

    writeStorage(remaining);
    set({ items: remaining, length: remaining.length });
    if (successCount > 0) {
      console.log(`[offlineQueue] flushed ${successCount} of ${items.length} entries`);
    }
    return successCount;
  },

  clear: () => {
    writeStorage([]);
    set({ items: [], length: 0 });
  },

  hydrate: () => {
    const items = readStorage();
    set({ items, length: items.length });
  },
}));

/**
 * Helper called from the api.ts response interceptor when a non-GET request
 * fails with a network error. Returns a synthetic axios-like response so
 * the caller's `await api.post(...)` resolves with the queue confirmation
 * instead of rejecting.
 */
export const enqueueOfflineRequest = (
  config: AxiosRequestConfig
): { data: { success: false; queued: true; uid: string }; status: 202; statusText: string } => {
  const method = (config.method || 'post').toUpperCase() as QueuedMethod;
  // Re-normalise the URL so it always begins with `/api/...` regardless of
  // whether the caller used `/users` or `users` or `/api/users`.
  const rawUrl = config.url || '';
  const url = rawUrl.startsWith('/api')
    ? rawUrl
    : `/api${rawUrl.startsWith('/') ? '' : '/'}${rawUrl}`;

  const data =
    typeof config.data === 'string'
      ? safeParseJson(config.data)
      : config.data ?? null;

  const headers: Record<string, string> = {};
  if (config.headers) {
    for (const [k, v] of Object.entries(config.headers)) {
      if (typeof v === 'string') headers[k] = v;
      else if (typeof v === 'number') headers[k] = String(v);
    }
  }

  useOfflineQueue.getState().push({
    method,
    url,
    data,
    headers,
  });

  // The most-recently-pushed entry's uid is the last item in the store.
  const uid = useOfflineQueue.getState().items.slice(-1)[0]?.uid ?? '';

  return {
    data: { success: false, queued: true, uid },
    status: 202,
    statusText: 'Accepted (Queued for replay)',
  };
};

const safeParseJson = (s: string): any => {
  try {
    return JSON.parse(s);
  } catch {
    return s;
  }
};
