import api from './api-client';

export interface OfflineReportLocation {
  latitude: number;
  longitude: number;
  accuracy?: number;
  region?: string;
  address?: string;
}

export interface OfflineAnonymousReport {
  id: string;
  description: string;
  media: File | null;
  location?: OfflineReportLocation;
  locationConsent: boolean;
  capturedAt: string;
  attempts: number;
  lastError?: string;
}

const DB_NAME = 'gcx-fshs-offline';
const DB_VERSION = 2;
const STORE_NAME = 'anonymous-reports';
let syncPromise: Promise<{ synced: number; remaining: number }> | null = null;

function createId(): string {
  return typeof crypto !== 'undefined' && 'randomUUID' in crypto
    ? crypto.randomUUID()
    : `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}

function openDatabase(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);
    request.onupgradeneeded = () => {
      const db = request.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        db.createObjectStore(STORE_NAME, { keyPath: 'id' });
      }
      if (!db.objectStoreNames.contains('issue-reports')) {
        db.createObjectStore('issue-reports', { keyPath: 'id' });
      }
    };
    request.onsuccess = () => {
      const db = request.result;
      db.onversionchange = () => db.close();
      resolve(db);
    };
    request.onerror = () => reject(request.error ?? new Error('Offline storage is unavailable.'));
    request.onblocked = () => reject(new Error('Offline storage is blocked by another app window.'));
  });
}

async function runStoreRequest<T>(
  mode: IDBTransactionMode,
  action: (store: IDBObjectStore) => IDBRequest<T>,
  storeName = STORE_NAME,
): Promise<T> {
  const db = await openDatabase();
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(storeName, mode);
    let result: T;
    let request: IDBRequest<T>;
    try {
      request = action(transaction.objectStore(storeName));
    } catch (error) {
      db.close();
      reject(error);
      return;
    }
    request.onsuccess = () => {
      result = request.result;
    };
    request.onerror = () => reject(request.error ?? new Error('Offline storage request failed.'));
    transaction.oncomplete = () => {
      db.close();
      resolve(result);
    };
    transaction.onerror = () => {
      db.close();
      reject(transaction.error ?? new Error('Offline storage transaction failed.'));
    };
    transaction.onabort = () => {
      db.close();
      reject(transaction.error ?? new Error('Offline storage transaction was aborted.'));
    };
  });
}

export async function queueAnonymousReport(
  report: Omit<OfflineAnonymousReport, 'id' | 'attempts'> & { id?: string },
): Promise<OfflineAnonymousReport> {
  const item: OfflineAnonymousReport = {
    ...report,
    id: report.id || createId(),
    attempts: 0,
  };
  await runStoreRequest('readwrite', store => store.put(item));
  notifyOutbox('queued', { id: item.id });
  return item;
}

export async function listAnonymousReports(): Promise<OfflineAnonymousReport[]> {
  const items = await runStoreRequest<OfflineAnonymousReport[]>('readonly', store => store.getAll());
  return items.sort((a, b) => a.capturedAt.localeCompare(b.capturedAt));
}

async function updateAnonymousReport(item: OfflineAnonymousReport): Promise<void> {
  await runStoreRequest('readwrite', store => store.put(item));
}

async function deleteAnonymousReport(id: string): Promise<void> {
  await runStoreRequest('readwrite', store => store.delete(id));
}

function buildFormData(item: OfflineAnonymousReport): FormData {
  const formData = new FormData();
  formData.append('client_submission_id', item.id);
  formData.append('description', item.description);
  formData.append('location_consent', item.locationConsent ? 'true' : 'false');
  formData.append('captured_at', item.capturedAt);
  if (item.media) {
    formData.append('media', item.media, item.media.name);
  }
  if (item.locationConsent && item.location) {
    formData.append('latitude', String(item.location.latitude));
    formData.append('longitude', String(item.location.longitude));
    if (item.location.accuracy !== undefined) formData.append('accuracy', String(item.location.accuracy));
    if (item.location.region) formData.append('region', item.location.region);
    if (item.location.address) formData.append('address', item.location.address);
  }
  return formData;
}

async function submitQueuedReport(item: OfflineAnonymousReport): Promise<void> {
  await api.post('/anonymous-reports', buildFormData(item), {
    headers: { 'Content-Type': 'multipart/form-data' },
    timeout: 60_000,
  });
}

export async function submitOrQueueAnonymousReport(
  input: Omit<OfflineAnonymousReport, 'id' | 'attempts'>,
): Promise<{ queued: boolean; id: string; response?: unknown }> {
  const id = createId();
  const item: OfflineAnonymousReport = { ...input, id, attempts: 0 };

  if (typeof navigator !== 'undefined' && !navigator.onLine) {
    await queueAnonymousReport(item);
    return { queued: true, id };
  }

  try {
    await submitQueuedReport(item);
    return { queued: false, id };
  } catch (error: any) {
    const isNetworkFailure = !error?.response || error?.code === 'ERR_NETWORK' || error?.code === 'ECONNABORTED';
    if (!isNetworkFailure) {
      throw error;
    }
    await queueAnonymousReport(item);
    return { queued: true, id };
  }
}

export async function syncAnonymousReportOutbox(): Promise<{ synced: number; remaining: number }> {
  if (syncPromise) return syncPromise;
  syncPromise = (async () => {
    if (typeof navigator !== 'undefined' && !navigator.onLine) {
      const remaining = (await listAnonymousReports()).length;
      return { synced: 0, remaining };
    }

    const items = await listAnonymousReports();
    let synced = 0;
    for (const item of items) {
      try {
        await submitQueuedReport(item);
        await deleteAnonymousReport(item.id);
        synced++;
      } catch (error: any) {
        const status = error?.response?.status;
        await updateAnonymousReport({
          ...item,
          attempts: item.attempts + 1,
          lastError: status ? `Server returned ${status}` : 'Waiting for a stable connection',
        });
        if (!status || status === 401 || status >= 500) break;
      }
    }

    const remaining = (await listAnonymousReports()).length;
    notifyOutbox('synced', { synced, remaining });
    return { synced, remaining };
  })().finally(() => {
    syncPromise = null;
  });
  return syncPromise;
}

function notifyOutbox(type: string, detail: Record<string, unknown>): void {
  if (typeof window !== 'undefined') {
    window.dispatchEvent(new CustomEvent('gcx-anonymous-report-outbox', { detail: { type, ...detail } }));
  }
}

interface OfflineIssue {
  id: string;
  userId: number;
  url: string;
  description: string;
  screenshot: string | null;
}

let issueSync: Promise<void> | null = null;

export async function submitOrQueueIssue(input: Pick<OfflineIssue, 'url' | 'description' | 'screenshot'>): Promise<{ queued: boolean }> {
  const user = JSON.parse(localStorage.getItem('gcx_fshs_user') || 'null');
  if (!user?.id) throw new Error('Sign in before sending a report.');
  const entry: OfflineIssue = { ...input, id: createId(), userId: Number(user.id) };
  await runStoreRequest('readwrite', store => store.put(entry), 'issue-reports');
  if (!navigator.onLine) return { queued: true };
  await syncIssueReportOutbox();
  const remaining = await runStoreRequest<OfflineIssue | undefined>('readonly', store => store.get(entry.id), 'issue-reports');
  return { queued: !!remaining };
}

export async function syncIssueReportOutbox(): Promise<void> {
  if (issueSync) return issueSync;
  issueSync = (async () => {
    if (!navigator.onLine) return;
    const entries = await runStoreRequest<OfflineIssue[]>('readonly', store => store.getAll(), 'issue-reports');
    if (entries.length === 0) return;
    const me = (await api.get('/auth/me')).data.user;
    for (const entry of entries) {
      if (Number(me?.id) !== entry.userId) continue;
      try {
        await api.post('/support/report-issue', { ...entry, client_submission_id: entry.id }, { timeout: 60000 });
        await runStoreRequest('readwrite', store => store.delete(entry.id), 'issue-reports');
      } catch (error: any) {
        if (!error.response || error.response.status === 401 || error.response.status >= 500) break;
      }
    }
  })().finally(() => { issueSync = null; });
  return issueSync;
}
