import type { PostType } from '@insta-monitor/core';
import type { ScrapedPost, ScrapedProfile } from './types.js';

/* eslint-disable @typescript-eslint/no-explicit-any -- parsing untyped external JSON */

/**
 * Parse an Instagram `web_profile_info` response into a ScrapedProfile.
 * This is the swappable adapter (PRD NFR-2): if IG changes its response
 * shape, only this function needs updating.
 */
export function parseWebProfileInfo(json: unknown): ScrapedProfile {
  const user = (json as any)?.data?.user;
  if (!user || typeof user.username !== 'string') {
    throw new Error('Unexpected web_profile_info shape: data.user missing');
  }
  const media = user.edge_owner_to_timeline_media ?? { count: 0, edges: [] };
  const edges: any[] = Array.isArray(media.edges) ? media.edges : [];
  const posts = edges.map((e) => parseNode(e.node));

  return {
    username: user.username,
    displayName: user.full_name || null,
    followerCount: user.edge_followed_by?.count ?? 0,
    postCount: typeof media.count === 'number' ? media.count : posts.length,
    posts,
    isPrivate: user.is_private === true,
  };
}

function parseNode(node: any): ScrapedPost {
  const type: PostType = node.product_type === 'clips' ? 'reel' : 'post';
  const shortcode: string = node.shortcode ?? node.code ?? '';
  const caption: string | null = node.edge_media_to_caption?.edges?.[0]?.node?.text ?? null;
  const timestamp: number = node.taken_at_timestamp ?? node.taken_at ?? 0;

  return {
    shortcode,
    type,
    url: `https://www.instagram.com/${type === 'reel' ? 'reel' : 'p'}/${shortcode}/`,
    thumbnail: node.display_url ?? node.thumbnail_url ?? null,
    caption,
    uploadedAt: new Date(timestamp * 1000).toISOString(),
    viewCount: node.video_view_count ?? node.play_count ?? null,
    likeCount: node.like_and_view_counts_disabled
      ? null
      : (node.edge_media_preview_like?.count ?? node.edge_liked_by?.count ?? node.like_count ?? null),
    commentCount: node.edge_media_to_comment?.count ?? node.comment_count ?? 0,
  };
}

/**
 * Parse the logged-in GraphQL timeline connection
 * (data.xdt_api__v1__feed__user_timeline_graphql_connection) into posts.
 * web_profile_info returns empty edges when authenticated, so this is the
 * post source for logged-in collection.
 */
export function parseTimelineConnection(json: unknown): ScrapedPost[] {
  const conn = (json as any)?.data?.xdt_api__v1__feed__user_timeline_graphql_connection;
  const edges: any[] = Array.isArray(conn?.edges) ? conn.edges : [];
  return edges.map((e) => parseTimelineNode(e.node));
}

const SC_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';

/**
 * Convert an Instagram shortcode to its numeric media pk (base64 over IG's
 * alphabet). The per-media info endpoint (/api/v1/media/{pk}/info/) keys on the
 * numeric pk, not the shortcode. Returns null on any invalid character.
 */
export function shortcodeToPk(shortcode: string): string | null {
  if (!shortcode) return null;
  let n = 0n;
  for (const ch of shortcode) {
    const d = SC_ALPHABET.indexOf(ch);
    if (d < 0) return null;
    n = n * 64n + BigInt(d);
  }
  return n.toString();
}

/**
 * Extract a reel's current view count from a /api/v1/media/{pk}/info/ response.
 * IG exposes the unified "조회수" as play_count (== ig_play_count) on the media
 * item — the timeline feed returns null for the same reel, which is why this is
 * fetched per-media. Returns null when absent (e.g. photo posts).
 */
export function parseMediaInfoViewCount(json: unknown): number | null {
  const item = (json as any)?.items?.[0];
  if (!item) return null;
  const v = item.play_count ?? item.ig_play_count ?? item.view_count;
  return typeof v === 'number' ? v : null;
}

/**
 * Extract reel view counts from a reels-tab GraphQL response
 * (PolarisProfileReelsTabContentQuery → xdt_api__v1__clips__user__connection_v2).
 * This is the PASSIVE, un-flagged way to get 조회수: navigate /{user}/reels/ and read
 * the play_count the page's own query already returns — instead of forging a
 * per-reel /api/v1/media/{pk}/info/ request (a confirmed rate-limit/ban signal).
 * Returns {shortcode, viewCount} pairs; missing play_count → 0. [] on any bad shape.
 */
export function parseReelsClipsConnection(json: unknown): Array<{ shortcode: string; viewCount: number }> {
  const conn = (json as any)?.data?.xdt_api__v1__clips__user__connection_v2;
  const edges: any[] = Array.isArray(conn?.edges) ? conn.edges : [];
  const out: Array<{ shortcode: string; viewCount: number }> = [];
  for (const e of edges) {
    const media = e?.node?.media;
    if (!media?.code) continue;
    const v = media.play_count ?? media.ig_play_count ?? media.view_count;
    out.push({ shortcode: media.code, viewCount: typeof v === 'number' ? v : 0 });
  }
  return out;
}

function parseTimelineNode(node: any): ScrapedPost {
  const type: PostType = node.product_type === 'clips' ? 'reel' : 'post';
  const shortcode: string = node.code ?? node.shortcode ?? '';
  const caption: string | null = node.caption?.text ?? null;
  const timestamp: number = node.taken_at ?? node.taken_at_timestamp ?? 0;
  const thumbnail: string | null = node.image_versions2?.candidates?.[0]?.url ?? null;

  return {
    shortcode,
    type,
    url: `https://www.instagram.com/${type === 'reel' ? 'reel' : 'p'}/${shortcode}/`,
    thumbnail,
    caption,
    uploadedAt: new Date(timestamp * 1000).toISOString(),
    // IG's feed/timeline node returns view_count=null for reels; the real number
    // lives on the per-media endpoint, so the collector backfills it afterwards
    // via fetchReelViews()/parseMediaInfoViewCount(). Kept here for the rare case
    // the feed does include it.
    viewCount: node.view_count ?? node.play_count ?? null,
    likeCount: node.like_and_view_counts_disabled
      ? null
      : (node.like_count ?? node.edge_media_preview_like?.count ?? node.edge_liked_by?.count ?? null),
    commentCount: node.comment_count ?? 0,
  };
}
