import { FieldType, type Segment } from "@hamradio/packet";

import { DO_NOT_ARCHIVE_MARKER, DataType, type IPosition, type Payload, type PositionPayload } from "./frame.types";
import { base91ToNumber, feetToMeters } from "./parser";
import { attachExtras, decodeCommentExtras } from "./payload.extras";
import Position from "./position";
import Timestamp from "./timestamp";

export const decodePositionPayload = (
  dataType: string,
  raw: string,
  withStructure: boolean = false
): { payload: Payload | null; segment?: Segment[] } => {
  try {
    const hasTimestamp = dataType === "/" || dataType === "@";
    const messaging = dataType === "=" || dataType === "@";
    let offset = 1; // Skip data type identifier

    // Build structure as we parse
    const structure: Segment[] = withStructure ? [] : [];

    let timestamp: Timestamp | undefined = undefined;

    // Parse timestamp if present (7 characters: DDHHMMz or HHMMSSh or MMDDHMMM)
    if (hasTimestamp) {
      if (raw.length < 8) return { payload: null };
      const timeStr = raw.substring(offset, offset + 7);
      const { timestamp: parsedTimestamp, segment: timestampSegment } = Timestamp.fromString(timeStr, withStructure);
      timestamp = parsedTimestamp;

      if (timestampSegment) {
        structure.push(timestampSegment);
      }

      offset += 7;
    }

    // Need at least enough characters for compressed position (13) or
    // uncompressed (19). Allow parsing to continue if compressed-length is present.
    if (raw.length < offset + 13) return { payload: null };

    // Check if compressed format
    const isCompressed = isCompressedPosition(raw.substring(offset));

    let position: Position;
    let comment = "";

    if (isCompressed) {
      // Compressed format: /YYYYXXXX$csT
      const { position: compressed, segment: compressedSegment } = parseCompressedPosition(
        raw.substring(offset),
        withStructure
      );
      if (!compressed) return { payload: null };

      position = new Position({
        latitude: compressed.latitude,
        longitude: compressed.longitude,
        symbol: compressed.symbol
      });

      if (compressed.altitude !== undefined) {
        position.altitude = compressed.altitude;
      }

      if (compressedSegment) {
        structure.push(compressedSegment);
      }

      offset += 13; // Compressed position is 13 chars
      comment = raw.substring(offset);
    } else {
      // Uncompressed format: DDMMmmH/DDDMMmmH$
      const { position: uncompressed, segment: uncompressedSegment } = parseUncompressedPosition(
        raw.substring(offset),
        withStructure
      );
      if (!uncompressed) return { payload: null };

      position = new Position({
        latitude: uncompressed.latitude,
        longitude: uncompressed.longitude,
        symbol: uncompressed.symbol
      });

      if (uncompressed.ambiguity !== undefined) {
        position.ambiguity = uncompressed.ambiguity;
      }

      if (uncompressedSegment) {
        structure.push(uncompressedSegment);
      }

      offset += 19; // Uncompressed position is 19 chars
      comment = raw.substring(offset);
    }

    // Extract Altitude, CSE/SPD, RNG and PHG tokens and optionally emit sections
    const remainder = comment; // Use the remaining comment text for parsing extras
    const doNotArchive = remainder.includes(DO_NOT_ARCHIVE_MARKER);
    const extras = decodeCommentExtras(remainder, withStructure);
    comment = extras.comment;

    if (comment) {
      position.comment = comment;

      // Emit comment section as we parse
      if (withStructure) {
        structure.push({
          name: "comment",
          data: new TextEncoder().encode(remainder).buffer,
          isString: true,
          fields: extras.fields || []
        });
      }
    } else if (withStructure && extras.fields) {
      // No free-text comment, but extras were present: emit a comment section containing only fields
      structure.push({
        name: "comment",
        data: new TextEncoder().encode("").buffer,
        isString: true,
        fields: extras.fields || []
      });
    }

    let payloadType:
      | DataType.PositionNoTimestampNoMessaging
      | DataType.PositionNoTimestampWithMessaging
      | DataType.PositionWithTimestampNoMessaging
      | DataType.PositionWithTimestampWithMessaging;
    switch (dataType) {
      case "!":
        payloadType = DataType.PositionNoTimestampNoMessaging;
        break;
      case "=":
        payloadType = DataType.PositionNoTimestampWithMessaging;
        break;
      case "/":
        payloadType = DataType.PositionWithTimestampNoMessaging;
        break;
      case "@":
        payloadType = DataType.PositionWithTimestampWithMessaging;
        break;
      default:
        return { payload: null };
    }

    const payload: PositionPayload = {
      type: payloadType,
      doNotArchive,
      timestamp,
      position,
      messaging
    };
    attachExtras(payload, extras);

    if (withStructure) {
      return { payload, segment: structure };
    }

    return { payload };
  } catch {
    return { payload: null };
  }
};

export const isCompressedPosition = (data: string): boolean => {
  if (data.length < 13) return false;

  // First prefer uncompressed detection by attempting an uncompressed parse.
  // Uncompressed APRS positions do not have a fixed symbol table separator;
  // position 8 is a symbol table identifier and may vary.
  if (data.length >= 19) {
    const uncompressed = parseUncompressedPosition(data, false);
    if (uncompressed.position) {
      return false;
    }
  }

  // For compressed format, check if the position part looks like base-91 encoded data
  // Compressed format: STYYYYXXXXcsT where ST is symbol table/code
  // Base-91 chars are in range 33-124 (! to |)
  const lat1 = data.charCodeAt(1);
  const lat2 = data.charCodeAt(2);
  const lon1 = data.charCodeAt(5);
  const lon2 = data.charCodeAt(6);

  return (
    lat1 >= 33 && lat1 <= 124 && lat2 >= 33 && lat2 <= 124 && lon1 >= 33 && lon1 <= 124 && lon2 >= 33 && lon2 <= 124
  );
};

export const parseCompressedPosition = (
  data: string,
  withStructure: boolean = false
): {
  position: IPosition | null;
  segment?: Segment;
} => {
  if (data.length < 13) return { position: null };

  const symbolTable = data.charAt(0);
  const symbolCode = data.charAt(9);

  // Extract base-91 encoded position (4 characters each)
  const latStr = data.substring(1, 5);
  const lonStr = data.substring(5, 9);

  try {
    // Decode base-91 encoded latitude and longitude
    const latBase91 = base91ToNumber(latStr);
    const lonBase91 = base91ToNumber(lonStr);

    // Convert to degrees
    const latitude = 90 - latBase91 / 380926;
    const longitude = -180 + lonBase91 / 190463;

    const result: IPosition = {
      latitude,
      longitude,
      symbol: {
        table: symbolTable,
        code: symbolCode
      }
    };

    // Check for compressed altitude (csT format)
    const cs = data.charAt(10);
    const t = data.charCodeAt(11);

    if (cs === " " && t >= 33 && t <= 124) {
      // Compressed altitude: altitude = 1.002^(t-33) feet
      const altFeet = Math.pow(1.002, t - 33);
      result.altitude = feetToMeters(altFeet); // Convert to meters
    }

    const section: Segment | undefined = withStructure
      ? {
          name: "position",
          data: new TextEncoder().encode(data.substring(0, 13)).buffer,
          isString: true,
          fields: [
            { type: FieldType.CHAR, length: 1, name: "symbol table" },
            { type: FieldType.STRING, length: 4, name: "latitude" },
            { type: FieldType.STRING, length: 4, name: "longitude" },
            { type: FieldType.CHAR, length: 1, name: "symbol code" },
            { type: FieldType.CHAR, length: 1, name: "course/speed type" },
            { type: FieldType.CHAR, length: 1, name: "course/speed value" },
            { type: FieldType.CHAR, length: 1, name: "altitude" }
          ]
        }
      : undefined;

    return { position: result, segment: section };
  } catch {
    return { position: null };
  }
};

export const parseUncompressedPosition = (
  data: string,
  withStructure: boolean = false
): {
  position: IPosition | null;
  segment?: Segment;
} => {
  if (data.length < 19) return { position: null };

  // Format: DDMMmmH/DDDMMmmH$ where H is hemisphere, $ is symbol code
  // Positions: 0-7 (latitude), 8 (symbol table), 9-17 (longitude), 18 (symbol code)
  // Spaces may replace rightmost digits for ambiguity/privacy

  const latStr = data.substring(0, 8); // DDMMmmH (8 chars: 49 03.50 N)
  const symbolTable = data.charAt(8);
  const lonStr = data.substring(9, 18); // DDDMMmmH (9 chars: 072 01.75 W)
  const symbolCode = data.charAt(18);

  // Count and handle ambiguity (spaces in minutes part replace rightmost digits)
  let ambiguity = 0;
  const latSpaceCount = (latStr.match(/ /g) || []).length;
  const lonSpaceCount = (lonStr.match(/ /g) || []).length;

  if (latSpaceCount > 0 || lonSpaceCount > 0) {
    // Use the maximum space count (they should be the same, but be defensive)
    ambiguity = Math.max(latSpaceCount, lonSpaceCount);
  }

  // Replace spaces with zeros for parsing
  const latStrNormalized = latStr.replace(/ /g, "0");
  const lonStrNormalized = lonStr.replace(/ /g, "0");

  // Parse latitude
  const latDeg = parseInt(latStrNormalized.substring(0, 2), 10);
  const latMin = parseFloat(latStrNormalized.substring(2, 7));
  const latHem = latStrNormalized.charAt(7);

  if (isNaN(latDeg) || isNaN(latMin)) return { position: null };
  if (latHem !== "N" && latHem !== "S") return { position: null };

  let latitude = latDeg + latMin / 60;
  if (latHem === "S") latitude = -latitude;

  // Parse longitude
  const lonDeg = parseInt(lonStrNormalized.substring(0, 3), 10);
  const lonMin = parseFloat(lonStrNormalized.substring(3, 8));
  const lonHem = lonStrNormalized.charAt(8);

  if (isNaN(lonDeg) || isNaN(lonMin)) return { position: null };
  if (lonHem !== "E" && lonHem !== "W") return { position: null };

  let longitude = lonDeg + lonMin / 60;
  if (lonHem === "W") longitude = -longitude;

  const result: IPosition = {
    latitude,
    longitude,
    symbol: {
      table: symbolTable,
      code: symbolCode
    }
  };

  if (ambiguity > 0) {
    result.ambiguity = ambiguity;
  }

  const segment: Segment | undefined = withStructure
    ? {
        name: "position",
        data: new TextEncoder().encode(data.substring(0, 19)).buffer,
        isString: true,
        fields: [
          { type: FieldType.STRING, length: 8, name: "latitude" },
          { type: FieldType.CHAR, length: 1, name: "symbol table" },
          { type: FieldType.STRING, length: 9, name: "longitude" },
          { type: FieldType.CHAR, length: 1, name: "symbol code" }
        ]
      }
    : undefined;

  return { position: result, segment };
};

export default decodePositionPayload;
