import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type PropsWithChildren,
} from 'react';
import { getSavedNotificationDeviceId } from '../../../app/main/notificationIdentity';
import { sendClientNotification } from '../../../app/main/notificationApi';
import type {
  GpsAnchor,
  GpsConfig,
  GpsCoordinates,
  GpsGeofenceEvent,
  GpsLayerSnapshot,
  GpsPermissionState,
} from '../types';

type GpsLayerContextValue = GpsLayerSnapshot & {
  enableGps: () => void;
  disableGps: () => void;
  toggleGps: () => void;
  saveCurrentAnchor: (name: string) => { ok: boolean; message?: string; anchor?: GpsAnchor };
  removeAnchor: (anchorId: string) => void;
  selectAnchor: (anchorId: string | null) => void;
  updateConfig: (patch: Partial<GpsConfig>) => void;
  clearEvents: () => void;
};

const GPS_STORAGE_KEY = 'gps-layer:anchors';
const GPS_CONFIG_STORAGE_KEY = 'gps-layer:config';
const GPS_SELECTED_STORAGE_KEY = 'gps-layer:selected-anchor';
const GPS_EVENTS_LIMIT = 24;
const DEFAULT_CONFIG: GpsConfig = {
  notifyOnEnter: true,
  notifyOnExit: true,
  radiusMeters: 120,
};

const GpsLayerContext = createContext<GpsLayerContextValue | null>(null);

function clampRadiusMeters(value: number) {
  if (!Number.isFinite(value)) {
    return DEFAULT_CONFIG.radiusMeters;
  }

  return Math.min(5000, Math.max(30, Math.round(value)));
}

function readJsonStorage<T>(key: string, fallback: T, parse: (value: unknown) => T) {
  if (typeof window === 'undefined') {
    return fallback;
  }

  try {
    const raw = window.localStorage.getItem(key);

    if (!raw) {
      return fallback;
    }

    return parse(JSON.parse(raw));
  } catch {
    return fallback;
  }
}

function createAnchorId() {
  return `gps-anchor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}

function toCoordinates(position: GeolocationPosition): GpsCoordinates {
  return {
    latitude: position.coords.latitude,
    longitude: position.coords.longitude,
    accuracy: Number.isFinite(position.coords.accuracy) ? position.coords.accuracy : null,
    altitude: Number.isFinite(position.coords.altitude ?? NaN) ? position.coords.altitude ?? null : null,
    heading: Number.isFinite(position.coords.heading ?? NaN) ? position.coords.heading ?? null : null,
    speed: Number.isFinite(position.coords.speed ?? NaN) ? position.coords.speed ?? null : null,
    timestamp: position.timestamp,
  };
}

function toRadians(value: number) {
  return (value * Math.PI) / 180;
}

function calculateDistanceMeters(
  latitudeA: number,
  longitudeA: number,
  latitudeB: number,
  longitudeB: number,
) {
  const earthRadiusMeters = 6371000;
  const deltaLatitude = toRadians(latitudeB - latitudeA);
  const deltaLongitude = toRadians(longitudeB - longitudeA);
  const sinLatitude = Math.sin(deltaLatitude / 2);
  const sinLongitude = Math.sin(deltaLongitude / 2);
  const root =
    sinLatitude * sinLatitude +
    Math.cos(toRadians(latitudeA)) *
      Math.cos(toRadians(latitudeB)) *
      sinLongitude *
      sinLongitude;

  return 2 * earthRadiusMeters * Math.atan2(Math.sqrt(root), Math.sqrt(1 - root));
}

function parseAnchors(value: unknown): GpsAnchor[] {
  if (!Array.isArray(value)) {
    return [];
  }

  return value.flatMap((item) => {
    if (!item || typeof item !== 'object') {
      return [];
    }

    const candidate = item as Partial<GpsAnchor>;

    if (
      typeof candidate.id !== 'string' ||
      typeof candidate.name !== 'string' ||
      typeof candidate.createdAt !== 'string' ||
      !Number.isFinite(candidate.latitude) ||
      !Number.isFinite(candidate.longitude)
    ) {
      return [];
    }

    return [
      {
        id: candidate.id,
        name: candidate.name,
        latitude: Number(candidate.latitude),
        longitude: Number(candidate.longitude),
        createdAt: candidate.createdAt,
      },
    ];
  });
}

function parseConfig(value: unknown): GpsConfig {
  if (!value || typeof value !== 'object') {
    return DEFAULT_CONFIG;
  }

  const candidate = value as Partial<GpsConfig>;

  return {
    notifyOnEnter: candidate.notifyOnEnter ?? DEFAULT_CONFIG.notifyOnEnter,
    notifyOnExit: candidate.notifyOnExit ?? DEFAULT_CONFIG.notifyOnExit,
    radiusMeters: clampRadiusMeters(candidate.radiusMeters ?? DEFAULT_CONFIG.radiusMeters),
  };
}

function sendGpsPushNotification(event: GpsGeofenceEvent) {
  const notificationDeviceId = getSavedNotificationDeviceId().trim();
  const targetDeviceIds = notificationDeviceId ? [notificationDeviceId] : [];
  const targetAppOrigin = typeof window !== 'undefined' ? window.location.origin.trim() : '';
  const targetAppDomain = typeof window !== 'undefined' ? window.location.hostname.trim() : '';

  return sendClientNotification({
    title: `GPS 거점 ${event.type === 'enter' ? 'In' : 'Out'} 알림`,
    body: `${event.anchorName} ${event.type === 'enter' ? '진입' : '이탈'} 감지 (${event.distanceMeters}m)`,
    threadId: 'gps-geofence',
    targetDeviceIds,
    targetAppOrigins: targetAppOrigin ? [targetAppOrigin] : undefined,
    targetAppDomains: targetAppDomain ? [targetAppDomain] : undefined,
    data: {
      category: 'gps-geofence',
      eventType: event.type,
      anchorId: event.anchorId,
      anchorName: event.anchorName,
      distanceMeters: String(event.distanceMeters),
      createdAt: event.createdAt,
    },
  }).catch(() => undefined);
}

export function GpsLayerProvider({ children }: PropsWithChildren) {
  const [enabled, setEnabled] = useState(false);
  const [permissionState, setPermissionState] = useState<GpsPermissionState>('idle');
  const [currentPosition, setCurrentPosition] = useState<GpsCoordinates | null>(null);
  const [anchors, setAnchors] = useState<GpsAnchor[]>(() => readJsonStorage(GPS_STORAGE_KEY, [], parseAnchors));
  const [config, setConfig] = useState<GpsConfig>(() =>
    readJsonStorage(GPS_CONFIG_STORAGE_KEY, DEFAULT_CONFIG, parseConfig),
  );
  const [selectedAnchorId, setSelectedAnchorId] = useState<string | null>(() =>
    readJsonStorage(GPS_SELECTED_STORAGE_KEY, null, (value) => (typeof value === 'string' ? value : null)),
  );
  const [anchorPresence, setAnchorPresence] = useState<Record<string, boolean>>({});
  const [events, setEvents] = useState<GpsGeofenceEvent[]>([]);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const watchIdRef = useRef<number | null>(null);
  const anchorPresenceRef = useRef<Record<string, boolean>>({});

  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    window.localStorage.setItem(GPS_STORAGE_KEY, JSON.stringify(anchors));
  }, [anchors]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    window.localStorage.setItem(GPS_CONFIG_STORAGE_KEY, JSON.stringify(config));
  }, [config]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    if (selectedAnchorId) {
      window.localStorage.setItem(GPS_SELECTED_STORAGE_KEY, selectedAnchorId);
      return;
    }

    window.localStorage.removeItem(GPS_SELECTED_STORAGE_KEY);
  }, [selectedAnchorId]);

  useEffect(() => {
    if (!selectedAnchorId) {
      return;
    }

    const selectedExists = anchors.some((anchor) => anchor.id === selectedAnchorId);

    if (!selectedExists) {
      setSelectedAnchorId(anchors[0]?.id ?? null);
    }
  }, [anchors, selectedAnchorId]);

  useEffect(() => {
    if (typeof navigator === 'undefined' || !('geolocation' in navigator)) {
      setPermissionState('unsupported');
      return;
    }

    if (!enabled) {
      if (watchIdRef.current !== null) {
        navigator.geolocation.clearWatch(watchIdRef.current);
        watchIdRef.current = null;
      }

      return;
    }

    setPermissionState((previous) => (previous === 'granted' ? previous : 'requesting'));
    setErrorMessage(null);

    watchIdRef.current = navigator.geolocation.watchPosition(
      (position) => {
        setCurrentPosition(toCoordinates(position));
        setPermissionState('granted');
        setErrorMessage(null);
      },
      (error) => {
        setPermissionState(error.code === error.PERMISSION_DENIED ? 'denied' : 'error');
        setErrorMessage(error.message || 'GPS 위치를 읽지 못했습니다.');
      },
      {
        enableHighAccuracy: true,
        maximumAge: 5000,
        timeout: 10000,
      },
    );

    return () => {
      if (watchIdRef.current !== null) {
        navigator.geolocation.clearWatch(watchIdRef.current);
        watchIdRef.current = null;
      }
    };
  }, [enabled]);

  useEffect(() => {
    if (!currentPosition || anchors.length === 0) {
      anchorPresenceRef.current = {};
      setAnchorPresence({});
      return;
    }

    const nextPresence: Record<string, boolean> = {};
    const nextEvents: GpsGeofenceEvent[] = [];

    for (const anchor of anchors) {
      const distanceMeters = calculateDistanceMeters(
        currentPosition.latitude,
        currentPosition.longitude,
        anchor.latitude,
        anchor.longitude,
      );
      const isInside = distanceMeters <= config.radiusMeters;
      const previousPresence = anchorPresenceRef.current[anchor.id];

      nextPresence[anchor.id] = isInside;

      if (typeof previousPresence === 'boolean' && previousPresence !== isInside) {
        const type = isInside ? 'enter' : 'exit';
        const shouldNotify =
          (type === 'enter' && config.notifyOnEnter) || (type === 'exit' && config.notifyOnExit);

        const event: GpsGeofenceEvent = {
          id: `gps-event-${Date.now()}-${anchor.id}-${type}`,
          anchorId: anchor.id,
          anchorName: anchor.name,
          type,
          distanceMeters: Math.round(distanceMeters),
          createdAt: new Date().toISOString(),
        };

        nextEvents.push(event);

        if (shouldNotify) {
          void sendGpsPushNotification(event);
        }
      }
    }

    anchorPresenceRef.current = nextPresence;
    setAnchorPresence(nextPresence);

    if (nextEvents.length > 0) {
      setEvents((previous) => [...nextEvents.reverse(), ...previous].slice(0, GPS_EVENTS_LIMIT));
    }
  }, [anchors, config.notifyOnEnter, config.notifyOnExit, config.radiusMeters, currentPosition]);

  const value = useMemo<GpsLayerContextValue>(
    () => ({
      enabled,
      supported: typeof navigator !== 'undefined' && 'geolocation' in navigator,
      permissionState,
      currentPosition,
      anchors,
      config,
      selectedAnchorId,
      anchorPresence,
      events,
      errorMessage,
      enableGps: () => {
        setEnabled(true);
      },
      disableGps: () => {
        setEnabled(false);
      },
      toggleGps: () => {
        setEnabled((previous) => !previous);
      },
      saveCurrentAnchor: (name: string) => {
        const trimmedName = name.trim();

        if (!currentPosition) {
          return {
            ok: false,
            message: '현재 GPS 좌표가 없어 거점을 저장할 수 없습니다.',
          };
        }

        if (!trimmedName) {
          return {
            ok: false,
            message: '거점명을 입력해 주세요.',
          };
        }

        const nextAnchor: GpsAnchor = {
          id: createAnchorId(),
          name: trimmedName,
          latitude: currentPosition.latitude,
          longitude: currentPosition.longitude,
          createdAt: new Date().toISOString(),
        };

        setAnchors((previous) => [nextAnchor, ...previous]);
        setSelectedAnchorId(nextAnchor.id);

        return {
          ok: true,
          anchor: nextAnchor,
        };
      },
      removeAnchor: (anchorId) => {
        setAnchors((previous) => previous.filter((anchor) => anchor.id !== anchorId));
        setAnchorPresence((previous) => {
          const nextPresence = { ...previous };
          delete nextPresence[anchorId];
          anchorPresenceRef.current = nextPresence;
          return nextPresence;
        });
      },
      selectAnchor: (anchorId) => {
        setSelectedAnchorId(anchorId);
      },
      updateConfig: (patch) => {
        setConfig((previous) => ({
          notifyOnEnter: patch.notifyOnEnter ?? previous.notifyOnEnter,
          notifyOnExit: patch.notifyOnExit ?? previous.notifyOnExit,
          radiusMeters:
            patch.radiusMeters === undefined ? previous.radiusMeters : clampRadiusMeters(patch.radiusMeters),
        }));
      },
      clearEvents: () => {
        setEvents([]);
      },
    }),
    [anchorPresence, anchors, config, currentPosition, enabled, errorMessage, events, permissionState, selectedAnchorId],
  );

  return <GpsLayerContext.Provider value={value}>{children}</GpsLayerContext.Provider>;
}

export function useGpsLayerContext() {
  const context = useContext(GpsLayerContext);

  if (!context) {
    throw new Error('useGpsLayerContext must be used within a GpsLayerProvider.');
  }

  return context;
}
