const peerProtocolVersion = 2;

export class PhonePeerError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "PhonePeerError";
    this.status = status;
  }
}

function cleanText(value, maximumLength) {
  return typeof value === "string" ? value.trim().slice(0, maximumLength) : "";
}

function finiteNumber(value, fallback = 0) {
  const number = Number(value);
  return Number.isFinite(number) && number >= 0 ? number : fallback;
}

function privateIPv4(hostname) {
  const parts = hostname.split(".").map(Number);
  if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
  return parts[0] === 10
    || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
    || (parts[0] === 192 && parts[1] === 168)
    || (parts[0] === 169 && parts[1] === 254);
}

export function validatePeerRegistration(payload) {
  const deviceId = cleanText(payload?.deviceId, 240);
  const deviceName = cleanText(payload?.deviceName, 120) || "iPhone";
  const peerToken = cleanText(payload?.peerToken, 256);
  let baseURL;
  try {
    baseURL = new URL(cleanText(payload?.baseURL, 500));
  } catch {
    throw new PhonePeerError(400, "The phone peer address is invalid.");
  }
  if (!deviceId || peerToken.length < 32) throw new PhonePeerError(400, "The phone peer identity is incomplete.");
  if (
    baseURL.protocol !== "http:"
    || baseURL.port !== "47822"
    || baseURL.pathname !== "/"
    || baseURL.username
    || baseURL.password
    || baseURL.search
    || baseURL.hash
    || !privateIPv4(baseURL.hostname)
  ) {
    throw new PhonePeerError(400, "Phone peers must use the fixed Private Reel port on a private Wi-Fi IPv4 address.");
  }
  return {
    deviceId,
    deviceName,
    peerToken,
    baseURL: baseURL.origin,
    photos: Math.floor(finiteNumber(payload?.photos)),
    videos: Math.floor(finiteNumber(payload?.videos)),
    albums: Math.floor(finiteNumber(payload?.albums)),
  };
}

function normalizedAlbums(value) {
  if (!Array.isArray(value)) return [];
  return [...new Set(value.map((album) => cleanText(album, 160)).filter(Boolean))].slice(0, 100);
}

export function normalizePeerCatalog(payload, peer) {
  if (!Array.isArray(payload?.items) || payload.items.length > 25_000) {
    throw new PhonePeerError(502, "The phone returned an invalid live catalog.");
  }
  return payload.items.flatMap((item) => {
    const id = cleanText(item?.id, 20).toLowerCase();
    const kind = item?.kind === "video" ? "video" : item?.kind === "image" ? "image" : null;
    if (!/^[a-f0-9]{20}$/.test(id) || !kind) return [];
    const albums = normalizedAlbums(item.albums);
    const mediaRoot = `/phone/live/${id}`;
    return [{
      id,
      kind,
      title: cleanText(item.title, 240) || (kind === "video" ? "Untitled video" : "Untitled photo"),
      description: "",
      tags: item.favorite ? ["favorite"] : [],
      type: kind === "image" ? "photo" : "movie",
      rating: null,
      createdAt: cleanText(item.createdAt, 40) || null,
      duration: finiteNumber(item.duration),
      width: finiteNumber(item.width) || null,
      height: finiteNumber(item.height) || null,
      size: finiteNumber(item.size),
      codec: null,
      videoCodec: null,
      streamOptimized: false,
      optimizationStatus: "original",
      thumbnailUrl: `${mediaRoot}/thumbnail`,
      streamUrl: `${mediaRoot}/stream`,
      downloadUrl: `${mediaRoot}/download`,
      library: peer.deviceName,
      playlist: albums[0] || null,
      playlistPosition: null,
      origin: "phone",
      availability: "live",
      canStream: true,
      albums,
    }];
  });
}

export class PhonePeerRegistry {
  constructor({ ttlMs = 25_000, requestTimeoutMs = 8_000, fetchImpl = globalThis.fetch, now = Date.now } = {}) {
    this.ttlMs = ttlMs;
    this.requestTimeoutMs = requestTimeoutMs;
    this.fetchImpl = fetchImpl;
    this.now = now;
    this.peer = null;
  }

  register(payload) {
    const peer = validatePeerRegistration(payload);
    this.peer = { ...peer, lastSeenAt: this.now() };
    return this.status();
  }

  clear(deviceId = "") {
    if (!this.peer || (deviceId && this.peer.deviceId !== deviceId)) return false;
    this.peer = null;
    return true;
  }

  activePeer() {
    if (!this.peer) return null;
    if (this.now() - this.peer.lastSeenAt > this.ttlMs) {
      this.peer = null;
      return null;
    }
    return this.peer;
  }

  status() {
    const peer = this.activePeer();
    return peer ? {
      protocolVersion: peerProtocolVersion,
      connected: true,
      deviceId: peer.deviceId,
      deviceName: peer.deviceName,
      photos: peer.photos,
      videos: peer.videos,
      albums: peer.albums,
      expiresInMs: Math.max(0, this.ttlMs - (this.now() - peer.lastSeenAt)),
    } : { protocolVersion: peerProtocolVersion, connected: false };
  }

  async request(path, { method = "GET", range } = {}) {
    const peer = this.activePeer();
    if (!peer) throw new PhonePeerError(503, "The iPhone is not sharing on this Wi-Fi network.");
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
    timeout.unref?.();
    try {
      return await this.fetchImpl(`${peer.baseURL}${path}`, {
        method,
        redirect: "error",
        cache: "no-store",
        headers: {
          "X-Private-Reel-Peer-Token": peer.peerToken,
          ...(range ? { Range: range } : {}),
        },
        signal: controller.signal,
      });
    } catch {
      throw new PhonePeerError(503, "The iPhone stopped responding on Wi-Fi.");
    } finally {
      clearTimeout(timeout);
    }
  }

  async catalogItems() {
    const peer = this.activePeer();
    if (!peer) return [];
    const response = await this.request("/api/catalog");
    if (!response.ok) throw new PhonePeerError(503, `The iPhone live catalog returned ${response.status}.`);
    return normalizePeerCatalog(await response.json(), peer);
  }
}
