import { z } from "zod";
import type { AgentConfig } from "../config.js";

export type TrendCandidate = {
  name: string;
  url?: string;
  volume?: number;
  rank: number;
};

export type TrendResearch = TrendCandidate & {
  searchResponse: unknown;
};

const recordSchema = z.record(z.string(), z.unknown());

type Tweet = {
  tweet_id?: string;
  text?: string;
  favorite_count?: number;
  retweet_count?: number;
  quote_count?: number;
  reply_count?: number;
  view_count?: number;
  urls?: string[];
};

const actionResponseSchema = z.object({
  data: z.unknown().optional(),
  error: z.unknown().optional(),
});

const retryDelay = (attempt: number) => new Promise((resolve) => setTimeout(resolve, attempt * 500));

const executeAction = async (config: AgentConfig, actionId: string, input: Record<string, unknown>) => {
  if (!config.xApi.key) throw new Error("XAPI_KEY is not configured");
  const url = new URL("v1/actions/execute", `${config.xApi.baseUrl.replace(/\/$/, "")}/`);
  let lastError: Error | undefined;

  for (let attempt = 0; attempt < 3; attempt += 1) {
    if (attempt > 0) await retryDelay(attempt);

    let response: Response;
    try {
      response = await fetch(url, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "XAPI-Key": config.xApi.key,
          accept: "application/json",
        },
        body: JSON.stringify({ action_id: actionId, input }),
        signal: AbortSignal.timeout(30_000),
      });
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      continue;
    }

    const body = await response.json().catch(() => null) as unknown;
    if (!response.ok) {
      const detail = recordSchema.safeParse(body).success ? JSON.stringify(body).slice(0, 500) : response.statusText;
      const error = new Error(`xAPI ${response.status}: ${detail}`);
      if (response.status < 500 && response.status !== 408 && response.status !== 429) throw error;
      lastError = error;
      continue;
    }
    const parsed = actionResponseSchema.safeParse(body);
    if (!parsed.success) throw new Error("xAPI returned an invalid action response");
    if (parsed.data.error) {
      const detail = JSON.stringify(parsed.data.error).slice(0, 500);
      lastError = new Error(`xAPI action ${actionId} returned an error: ${detail}`);
      continue;
    }
    return parsed.data.data ?? body;
  }

  throw lastError ?? new Error(`xAPI action ${actionId} failed`);
};

const tweetsFromResponse = (raw: unknown): Tweet[] => {
  const root = recordSchema.safeParse(raw);
  if (!root.success) return [];
  const data = recordSchema.safeParse(root.data.data).success
    ? root.data.data as Record<string, unknown>
    : root.data;
  const tweets = data.tweets;
  if (!Array.isArray(tweets)) return [];
  return tweets.flatMap((tweet) => {
    const parsed = recordSchema.safeParse(tweet);
    if (!parsed.success) return [];
    return [parsed.data as Tweet];
  });
};

const engagement = (tweet: Tweet) =>
  (tweet.favorite_count ?? 0)
  + (tweet.retweet_count ?? 0) * 3
  + (tweet.quote_count ?? 0) * 2
  + (tweet.reply_count ?? 0)
  + Math.log10(Math.max(1, tweet.view_count ?? 0));

const topicFromTweet = (tweet: Tweet) => {
  const text = tweet.text?.trim() ?? "";
  const hashtags = [...text.matchAll(/#([a-zA-Z0-9_]{2,64})/g)].map((match) => match[1]);
  if (hashtags.length) return [...new Set(hashtags.map((tag) => `#${tag}`))];
  const compact = text.replace(/https?:\/\/\S+/g, "").replace(/\s+/g, " ").trim();
  return compact ? [compact.slice(0, 96)] : [];
};

export const extractTrendCandidates = (raw: unknown, maxCandidates: number): TrendCandidate[] => {
  const scores = new Map<string, { name: string; score: number; volume: number; url?: string }>();
  for (const tweet of tweetsFromResponse(raw)) {
    const score = engagement(tweet);
    for (const topic of topicFromTweet(tweet)) {
      const key = topic.toLowerCase();
      const current = scores.get(key);
      const url = tweet.tweet_id ? `https://x.com/i/web/status/${tweet.tweet_id}` : tweet.urls?.[0];
      scores.set(key, {
        name: current?.name ?? topic,
        score: (current?.score ?? 0) + score,
        volume: (current?.volume ?? 0) + 1,
        ...(current?.url ? { url: current.url } : url ? { url } : {}),
      });
    }
  }
  return [...scores.values()]
    .sort((left, right) => right.score - left.score)
    .slice(0, maxCandidates)
    .map((candidate, index) => ({ name: candidate.name, rank: index + 1, volume: candidate.volume, ...(candidate.url ? { url: candidate.url } : {}) }));
};

export const getTrends = async (config: AgentConfig): Promise<TrendCandidate[]> => {
  const response = await executeAction(config, "twitter.search", {
    raw_query: config.xApi.query,
    sort_by: "Top",
    count: 100,
    provider: "x",
  });
  return extractTrendCandidates(response, config.trendMaxCandidates);
};

export const researchTrends = async (config: AgentConfig, trends: TrendCandidate[]): Promise<TrendResearch[]> => {
  const settled = await Promise.allSettled(trends.map(async (trend) => {
    const searchResponse = await executeAction(config, "twitter.search", {
      raw_query: trend.name,
      sort_by: "Latest",
      count: 50,
      provider: "x",
    });
    return { ...trend, searchResponse };
  }));
  const research = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
  if (research.length === 0) {
    const failures = settled.flatMap((result) => result.status === "rejected"
      ? [result.reason instanceof Error ? result.reason.message : String(result.reason)]
      : []);
    throw new Error(`xAPI research failed for every candidate: ${failures.join("; ").slice(0, 1_000)}`);
  }
  return research;
};
