/**
 * Attachments Module - Zustand Store (§A File Attachments)
 *
 * Polymorphic file store fronting /api/attachments. The shape is just
 * list / upload / remove / preview — the heavy lifting (mime allowlist,
 * file size cap, RBAC) lives in the backend controller; this client
 * store focuses on cache + blob-URL preview.
 *
 * The blob URLs created in `preview()` are caller-owned — the modal
 * that receives them must call `URL.revokeObjectURL` on unmount, or
 * memory will leak across opens.
 */

import { create } from 'zustand';
import api from './api';
import { createAxiosUploadProgressHandler } from '../lib/uploadProgress';

export type AttachmentParentKind =
  | 'subject'
  | 'assignment'
  | 'homework'
  | 'test'
  | 'exam_paper'
  | 'submission'
  | 'marked_script';

export interface Attachment {
  id: number;
  uid: string;
  parent_kind: AttachmentParentKind | string;
  parent_id: number;
  original_filename: string;
  stored_filename: string;
  mime_type: string;
  size_bytes: number;
  uploaded_by: number;
  uploaded_at: string;
  caption?: string | null;
}

export interface PreviewResult {
  url: string;
  mime: string;
  name: string;
  size: number;
}

interface UploadProgress {
  current: number;
  total: number;
  fileName: string;
  loaded: number;
  totalBytes: number;
  percent: number;
}

interface AttachmentState {
  byParent: Record<string, Attachment[]>;
  loading: boolean;
  uploading: boolean;
  uploadProgress: UploadProgress | null;
  error: string | null;

  // Actions
  list: (parent_kind: string, parent_id: number) => Promise<Attachment[]>;
  upload: (
    parent_kind: string,
    parent_id: number,
    files: File[],
    caption?: string
  ) => Promise<Attachment[]>;
  remove: (uid: string) => Promise<void>;
  downloadUrl: (stored_filename: string) => string;
  preview: (uid: string) => Promise<PreviewResult>;
  getCached: (parent_kind: string, parent_id: number) => Attachment[];
  clearError: () => void;
  reset: () => void;
}

const cacheKey = (kind: string, id: number) => `${kind}:${id}`;

export const useAttachmentStore = create<AttachmentState>((set, get) => ({
  byParent: {},
  loading: false,
  uploading: false,
  uploadProgress: null,
  error: null,

  list: async (parent_kind, parent_id) => {
    const key = cacheKey(parent_kind, parent_id);
    set({ loading: true, error: null });
    try {
      const res = await api.get('/attachments', {
        params: { parent_kind, parent_id },
      });
      const rows: Attachment[] = Array.isArray(res.data) ? res.data : [];
      set(state => ({
        byParent: { ...state.byParent, [key]: rows },
        loading: false,
      }));
      return rows;
    } catch (err: any) {
      // Offline / network fallback: api.ts may have routed this to a
      // local SQLite shim. The shim's generic `SELECT * FROM <table>`
      // fallback DOES know about `attachments` (the schema is in the
      // offline table list), so on a server-down read it will return
      // whatever rows are cached locally. Treat a real network error
      // as a soft failure with no error toast — the page can still
      // render the (possibly stale) empty state.
      const empty: Attachment[] = [];
      set(state => ({
        byParent: { ...state.byParent, [key]: empty },
        loading: false,
        error: err?.response?.status && err.response.status !== 404
          ? err?.response?.data?.error || err?.message || 'Failed to load attachments'
          : null,
      }));
      return empty;
    }
  },

  upload: async (parent_kind, parent_id, files, caption) => {
    if (!files || files.length === 0) return [];

    set({ uploading: true, uploadProgress: { current: 0, total: files.length, fileName: files[0].name, loaded: 0, totalBytes: 0, percent: 0 }, error: null });
    try {
      const formData = new FormData();
      formData.append('parent_kind', parent_kind);
      formData.append('parent_id', String(parent_id));
      if (caption && caption.trim()) {
        formData.append('caption', caption.trim());
      }
      files.forEach((file, idx) => {
        formData.append('files', file, file.name);
        void idx;
      });

      const onUploadProgress = createAxiosUploadProgressHandler((percent) => {
        if (percent == null) return;
        set(state => ({
          uploadProgress: state.uploadProgress
            ? { ...state.uploadProgress, percent }
            : {
                current: 0,
                total: files.length,
                fileName: files[0].name,
                loaded: 0,
                totalBytes: 0,
                percent,
              },
        }));
      });

      const res = await api.post('/attachments', formData, {
        onUploadProgress,
      });
      const created: Attachment[] = Array.isArray(res.data) ? res.data : [res.data];

      set({ uploading: false, uploadProgress: null });

      // Refresh the cache for this parent so subsequent reads return
      // the merged list. We do a full list instead of merging locally
      // to keep the ordering and any server-side defaults in sync.
      try {
        await get().list(parent_kind, parent_id);
      } catch {
        // list() already updates the error state; swallow here.
      }

      return created;
    } catch (err: any) {
      set({
        uploading: false,
        uploadProgress: null,
        error: err?.response?.data?.error || err?.message || 'Upload failed',
      });
      throw err;
    }
  },

  remove: async (uid) => {
    set({ error: null });
    try {
      await api.delete(`/attachments/${uid}`);
      set(state => {
        const next: Record<string, Attachment[]> = {};
        for (const [key, rows] of Object.entries(state.byParent)) {
          next[key] = rows.filter(r => r.uid !== uid);
        }
        return { byParent: next };
      });
    } catch (err: any) {
      set({
        error: err?.response?.data?.error || err?.message || 'Delete failed',
      });
      throw err;
    }
  },

  // No fetch needed: the existing express.static mount at
  // /api/uploads serves the binary directly. Same-origin GET works
  // because the auth header is only required for /api/attachments
  // (and the proxy passes cookies / Authorization through).
  downloadUrl: (stored_filename) => `/api/uploads/${encodeURIComponent(stored_filename)}`,

  preview: async (uid) => {
    set({ loading: true, error: null });
    try {
      const metaRes = await api.get(`/attachments/${uid}`);
      const row: Attachment = metaRes.data;

      const blobRes = await api.get(`/uploads/${encodeURIComponent(row.stored_filename)}`, {
        responseType: 'blob',
      });
      const blob: Blob = blobRes.data;
      const url = URL.createObjectURL(blob);

      set({ loading: false });
      return {
        url,
        mime: row.mime_type,
        name: row.original_filename,
        size: row.size_bytes,
      };
    } catch (err: any) {
      set({
        loading: false,
        error: err?.response?.data?.error || err?.message || 'Preview failed',
      });
      throw err;
    }
  },

  getCached: (parent_kind, parent_id) => {
    return get().byParent[cacheKey(parent_kind, parent_id)] || [];
  },

  clearError: () => set({ error: null }),

  reset: () => set({ byParent: {}, loading: false, uploading: false, uploadProgress: null, error: null }),
}));

export default useAttachmentStore;
