import { useCallback, useEffect, useRef, useState } from 'react';
import type { TFunction } from 'i18next';

import type { Bounds, FeatureFilters, FeatureMeta } from '../../../types';
import { apiUrl, authHeaders, buildFilterString, logNonAbortError } from '../../../lib/api';
import { trackEvent } from '../../../lib/analytics';
import type { ExportNotice, ExportState } from './types';
import { resolveTransitVariant, type TravelTimeEntry } from '../../../hooks/useTravelTime';
import { buildTravelParam, dedupeTravelTimeEntries } from '../../../lib/travel-params';
import type { OverlayId } from '../../../lib/overlays';

const EXPORT_FILE_NAME = 'perfect-postcode-export.xlsx';
const EXPORT_TIMEOUT_MS = 150_000;
const EXPORT_NOTICE_MS = 6000;
const EXPORT_ERROR_NOTICE_MS = 9000;

function getExportFileName(res: Response): string {
  const disposition = res.headers.get('content-disposition');
  if (!disposition) return EXPORT_FILE_NAME;

  const encodedMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i);
  if (encodedMatch?.[1]) {
    try {
      return decodeURIComponent(encodedMatch[1].trim());
    } catch {
      return encodedMatch[1].trim();
    }
  }

  const match = disposition.match(/filename="?([^";]+)"?/i);
  return match?.[1]?.trim() || EXPORT_FILE_NAME;
}

async function getExportErrorMessage(res: Response): Promise<string> {
  const fallback = `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`;
  const contentType = res.headers.get('content-type') ?? '';

  try {
    if (contentType.includes('application/json')) {
      const data: unknown = await res.json();
      if (data && typeof data === 'object') {
        const record = data as Record<string, unknown>;
        const message = record.message ?? record.error;
        if (typeof message === 'string' && message.trim()) return message.trim();
      }
      return fallback;
    }

    const text = await res.text();
    return text.trim() || fallback;
  } catch {
    return fallback;
  }
}

function triggerExportDownload(blob: Blob, fileName: string): void {
  const objectUrl = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = objectUrl;
  link.download = fileName;
  link.rel = 'noopener';
  link.style.display = 'none';

  document.body.appendChild(link);
  link.click();
  link.remove();

  window.setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
}

function appendTravelStateParams(params: URLSearchParams, entries: TravelTimeEntry[]): void {
  for (const entry of dedupeTravelTimeEntries(entries)) {
    if (!entry.slug) continue;
    const serverMode = resolveTransitVariant(entry);
    let value = `${serverMode}:${entry.slug}:${encodeURIComponent(entry.label)}`;
    if (entry.useBest) value += ':b';
    if (entry.timeRange) {
      value += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
    }
    params.append('tt', value);
  }
}

interface UseExportControllerOptions {
  bounds: Bounds | null;
  filters: FeatureFilters;
  features: FeatureMeta[];
  travelTimeEntries: TravelTimeEntry[];
  selectedOverlays: Set<OverlayId>;
  shareCode?: string;
  t: TFunction;
  onExportStateChange?: (state: ExportState) => void;
}

export function useExportController({
  bounds,
  filters,
  features,
  travelTimeEntries,
  selectedOverlays,
  shareCode,
  t,
  onExportStateChange,
}: UseExportControllerOptions) {
  const [exporting, setExporting] = useState(false);
  const [exportNotice, setExportNotice] = useState<ExportNotice | null>(null);
  const exportNoticeTimeoutRef = useRef<number | null>(null);

  const clearExportNoticeTimer = useCallback(() => {
    if (exportNoticeTimeoutRef.current !== null) {
      window.clearTimeout(exportNoticeTimeoutRef.current);
      exportNoticeTimeoutRef.current = null;
    }
  }, []);

  const clearExportNotice = useCallback(() => {
    clearExportNoticeTimer();
    setExportNotice(null);
  }, [clearExportNoticeTimer]);

  const showExportNotice = useCallback(
    (notice: ExportNotice) => {
      clearExportNoticeTimer();
      setExportNotice(notice);
      exportNoticeTimeoutRef.current = window.setTimeout(
        () => {
          setExportNotice(null);
          exportNoticeTimeoutRef.current = null;
        },
        notice.kind === 'error' ? EXPORT_ERROR_NOTICE_MS : EXPORT_NOTICE_MS
      );
    },
    [clearExportNoticeTimer]
  );

  useEffect(() => clearExportNoticeTimer, [clearExportNoticeTimer]);

  const handleExport = useCallback(
    (options?: { postcodes?: string[] }) => {
      if (exporting) return;

      const postcodeList = options?.postcodes?.map((p) => p.trim()).filter(Boolean) ?? [];
      const isListMode = postcodeList.length > 0;

      if (!isListMode && !bounds) {
        showExportNotice({ kind: 'error', message: t('header.exportUnavailable') });
        return;
      }

      const params = new URLSearchParams();
      if (isListMode) {
        params.set('postcodes', postcodeList.join(','));
        // Pass the active filters so the export keeps its two sheets — "Selected"
        // (the filtered feature columns) and "All Data" — even in list mode. The
        // server uses them only to pick columns; the supplied postcodes still
        // drive which rows appear.
        const filterStr = buildFilterString(filters, features);
        if (filterStr) params.set('filters', filterStr);
        // Travel times are per-postcode, so include them as columns here too.
        // The server ignores the time-range filter in list mode (the supplied
        // postcodes drive the rows), but still shows each one's travel time.
        const travelParam = buildTravelParam(travelTimeEntries);
        if (travelParam) params.set('travel', travelParam);
        appendTravelStateParams(params, travelTimeEntries);
        if (shareCode) params.set('share', shareCode);
      } else {
        const { south, west, north, east } = bounds!;
        params.set('bounds', `${south},${west},${north},${east}`);
        const filterStr = buildFilterString(filters, features);
        if (filterStr) params.set('filters', filterStr);
        const travelParam = buildTravelParam(travelTimeEntries);
        if (travelParam) params.set('travel', travelParam);
        appendTravelStateParams(params, travelTimeEntries);
        for (const overlay of selectedOverlays) {
          params.append('overlay', overlay);
        }
        if (shareCode) params.set('share', shareCode);
      }
      const url = apiUrl('export', params);

      const controller = new AbortController();
      let timedOut = false;
      const timeoutId = window.setTimeout(() => {
        timedOut = true;
        controller.abort();
      }, EXPORT_TIMEOUT_MS);

      setExporting(true);
      clearExportNotice();

      void (async () => {
        try {
          const res = await fetch(url, authHeaders({ signal: controller.signal }));
          if (!res.ok) throw new Error(await getExportErrorMessage(res));

          const blob = await res.blob();
          if (blob.size === 0) throw new Error(t('header.exportEmpty'));

          triggerExportDownload(blob, getExportFileName(res));
          trackEvent('Export');
          showExportNotice({ kind: 'success', message: t('header.exportReady') });
        } catch (err) {
          if (!timedOut) logNonAbortError('Export failed', err);
          const detail = err instanceof Error && err.message.trim() ? ` ${err.message}` : '';
          showExportNotice({
            kind: 'error',
            message: timedOut ? t('header.exportTimedOut') : `${t('header.exportFailed')}${detail}`,
          });
        } finally {
          window.clearTimeout(timeoutId);
          setExporting(false);
        }
      })();
    },
    [
      bounds,
      clearExportNotice,
      exporting,
      features,
      filters,
      selectedOverlays,
      shareCode,
      showExportNotice,
      t,
      travelTimeEntries,
    ]
  );

  useEffect(() => {
    onExportStateChange?.({ onExport: handleExport, exporting });
  }, [handleExport, exporting, onExportStateChange]);

  return {
    exporting,
    exportNotice,
    clearExportNotice,
    handleExport,
  };
}
