import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import type { Frame } from '../../protocols/aprs';
import { parseFrame } from '../../protocols/aprs';
import API from '../../services/API';
import APRSServiceImpl, { type FetchedAPRSPacket } from '../../services/APRSService';
import APRSStream, { type APRSMessage } from '../../services/APRSStream';

export interface APRSPacketRecord {
  timestamp: Date;
  raw: string;
  frame: Frame;
  latitude?: number;
  longitude?: number;
  altitude?: number;
  speed?: number;
  course?: number;
  comment?: string;
  symbol?: string;
  radioName?: string;
  hasAPILocation?: boolean;
}

interface APRSDataContextValue {
  packets: APRSPacketRecord[];
  streamReady: boolean;
}

const APRSDataContext = createContext<APRSDataContextValue | null>(null);

export const useAPRSData = (): APRSDataContextValue => {
  const context = useContext(APRSDataContext);
  if (!context) {
    throw new Error('useAPRSData must be used within APRSDataProvider');
  }
  return context;
};

const aprsService = new APRSServiceImpl(API);
const maxPacketCount = 500;

const fromNullableNumber = (value: unknown): number | undefined => {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }

  if (value && typeof value === 'object') {
    const candidate = value as { Float64?: number; Valid?: boolean };
    if (candidate.Valid && typeof candidate.Float64 === 'number' && Number.isFinite(candidate.Float64)) {
      return candidate.Float64;
    }
  }

  return undefined;
};

const extractAPRSDetails = (frame: Frame) => {
  const decoded = frame.decode();

  let latitude: number | undefined;
  let longitude: number | undefined;
  let altitude: number | undefined;
  let speed: number | undefined;
  let course: number | undefined;
  let comment: string | undefined;
  let symbol: string | undefined;

  if (decoded && typeof decoded === 'object') {
    if ('position' in decoded && decoded.position) {
      latitude = decoded.position.latitude;
      longitude = decoded.position.longitude;
      altitude = decoded.position.altitude;
      comment = decoded.position.comment;
      if (decoded.position.symbol) {
        symbol = `${decoded.position.symbol.table}${decoded.position.symbol.code}`;
      }
    }

    if ('altitude' in decoded && typeof decoded.altitude === 'number') {
      altitude = decoded.altitude;
    }

    if ('speed' in decoded && typeof decoded.speed === 'number') {
      speed = decoded.speed;
    }

    if ('course' in decoded && typeof decoded.course === 'number') {
      course = decoded.course;
    }

    if ('text' in decoded && typeof decoded.text === 'string' && !comment) {
      comment = decoded.text;
    }
  }

  return {
    latitude,
    longitude,
    altitude,
    speed,
    course,
    comment,
    symbol,
  };
};

const parseTimestamp = (input: unknown): Date => {
  if (typeof input === 'string' && input.trim().length > 0) {
    const parsed = new Date(input);
    if (!Number.isNaN(parsed.getTime())) {
      return parsed;
    }
  }

  return new Date();
};

const normalizeCoordinates = (latitude: number | undefined, longitude: number | undefined): { latitude?: number; longitude?: number } => {
  // Validate and fix potentially swapped coordinates
  if (latitude === undefined || longitude === undefined) {
    return { latitude, longitude };
  }

  // Valid ranges: latitude [-90, 90], longitude [-180, 180]
  const isValidLat = latitude >= -90 && latitude <= 90;
  const isValidLng = longitude >= -180 && longitude <= 180;

  // Both valid, no need to fix
  if (isValidLat && isValidLng) {
    return { latitude, longitude };
  }

  // Check if they're swapped
  const isSwappedValid = (longitude >= -90 && longitude <= 90) && (latitude >= -180 && latitude <= 180);
  if (isSwappedValid && (!isValidLat || !isValidLng)) {
    console.warn(`Coordinates appear to be swapped: [${latitude}, ${longitude}], fixing to [${longitude}, ${latitude}]`);
    return { latitude: longitude, longitude: latitude };
  }

  // One or both are invalid, return as-is (will be filtered out later)
  return { latitude: isValidLat ? latitude : undefined, longitude: isValidLng ? longitude : undefined };
};

const mergePackets = (incoming: APRSPacketRecord[], current: APRSPacketRecord[]): APRSPacketRecord[] => {
  const merged = [...incoming, ...current];
  const byKey = new Map<string, APRSPacketRecord>();

  merged.forEach((packet) => {
    const sourceSsid = packet.frame.source.ssid ?? 0;
    const key = `${packet.timestamp.toISOString()}|${packet.frame.source.call}|${sourceSsid}|${packet.raw}`;
    const existing = byKey.get(key);

    if (!existing) {
      byKey.set(key, packet);
      return;
    }

    if (!existing.hasAPILocation && packet.hasAPILocation) {
      byKey.set(key, packet);
    }
  });

  return Array.from(byKey.values())
    .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
    .slice(0, maxPacketCount);
};

const buildRecord = ({
  raw,
  timestamp,
  radioName,
  comment,
  latitude,
  longitude,
  symbol,
  preferProvidedLocation,
}: {
  raw: string;
  timestamp: Date;
  radioName?: string;
  comment?: string;
  latitude?: number;
  longitude?: number;
  symbol?: string;
  preferProvidedLocation?: boolean;
}): APRSPacketRecord | null => {
  try {
    const frame = parseFrame(raw);
    const details = extractAPRSDetails(frame);

    const hasProvidedLocation = latitude !== undefined && longitude !== undefined;
    const finalLat = hasProvidedLocation ? latitude : details.latitude;
    const finalLng = hasProvidedLocation ? longitude : details.longitude;
    const normalized = (preferProvidedLocation && hasProvidedLocation)
      ? { latitude: finalLat, longitude: finalLng }
      : normalizeCoordinates(finalLat, finalLng);

    return {
      timestamp,
      raw,
      frame,
      latitude: normalized.latitude,
      longitude: normalized.longitude,
      altitude: details.altitude,
      speed: details.speed,
      course: details.course,
      comment: comment ?? details.comment,
      symbol: symbol ?? details.symbol,
      radioName,
      hasAPILocation: preferProvidedLocation && hasProvidedLocation,
    };
  } catch {
    return null;
  }
};

const toRecordFromAPI = (packet: FetchedAPRSPacket): APRSPacketRecord | null => {
  const raw = packet.raw ?? packet.payload;
  if (!raw) {
    return null;
  }

  return buildRecord({
    raw,
    timestamp: parseTimestamp(packet.received_at ?? packet.timestamp ?? packet.time ?? packet.created_at),
    radioName: packet.radio?.name,
    comment: packet.comment,
    latitude: fromNullableNumber(packet.latitude),
    longitude: fromNullableNumber(packet.longitude),
    symbol: packet.symbol,
    preferProvidedLocation: true,
  });
};

const toRecordFromStream = (message: APRSMessage): APRSPacketRecord | null => {
  return buildRecord({
    raw: message.raw,
    timestamp: message.receivedAt,
    radioName: message.radioName,
  });
};

interface APRSDataProviderProps {
  children: React.ReactNode;
}

export const APRSDataProvider: React.FC<APRSDataProviderProps> = ({ children }) => {
  const [packets, setPackets] = useState<APRSPacketRecord[]>([]);
  const [streamReady, setStreamReady] = useState(false);
  const stream = useMemo(() => new APRSStream(false), []);

  useEffect(() => {
    let isMounted = true;

    const fetchPackets = async () => {
      try {
        const fetchedPackets = await aprsService.fetchPackets();
        if (!isMounted) {
          return;
        }

        const records = fetchedPackets
          .map(toRecordFromAPI)
          .filter((packet): packet is APRSPacketRecord => packet !== null);

        setPackets((prev) => mergePackets(records, prev));
      } catch (error) {
        console.error('Failed to fetch APRS packets:', error);
      }
    };

    fetchPackets();
    stream.connect();

    const unsubscribeState = stream.subscribeToState((state) => {
      setStreamReady(state.isConnected);
    });

    const unsubscribePackets = stream.subscribe<APRSMessage>('aprs/packet/#', (message) => {
      const record = toRecordFromStream(message);
      if (!record) {
        return;
      }

      setPackets((prev) => mergePackets([record], prev));
    });

    return () => {
      isMounted = false;
      unsubscribeState();
      unsubscribePackets();
      stream.disconnect();
    };
  }, [stream]);

  const value = useMemo(
    () => ({
      packets,
      streamReady,
    }),
    [packets, streamReady]
  );

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