const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour in milliseconds
const FETCH_TIMEOUT_MS = 10 * 1000; // 10 second timeout for API requests

interface CacheEntry<T> {
  data: T;
  timestamp: number;
}

const cache = new Map<string, CacheEntry<unknown>>();

/**
 * Memoized fetch that caches results for 1 hour.
 * Used to avoid rate limiting on GitHub API calls during builds.
 */
async function memoizedFetch<T>(
  cacheKey: string,
  fetchFn: () => Promise<T>
): Promise<T> {
  const now = Date.now();
  const cached = cache.get(cacheKey) as CacheEntry<T> | undefined;

  if (cached && now - cached.timestamp < CACHE_TTL_MS) {
    return cached.data;
  }

  const data = await fetchFn();
  // Only cache successful (non-null) results to allow retries on failures
  if (data !== null) {
    cache.set(cacheKey, { data, timestamp: now });
  }
  return data;
}

interface GitHubRepoResponse {
  stargazers_count: number;
  [key: string]: unknown;
}

interface GitHubReleaseResponse {
  tag_name: string;
  name: string;
  html_url: string;
  published_at: string;
  body?: string;
  [key: string]: unknown;
}

export interface GitHubCommit {
  sha: string;
  html_url: string;
  commit: { message: string; author?: { name?: string } | null };
  // null when the committer isn't a linked GitHub user.
  author: { login: string } | null;
}

interface GitHubCompareResponse {
  commits: GitHubCommit[];
  [key: string]: unknown;
}

/**
 * Fetches GitHub repository data with memoization.
 * Results are cached for 1 hour to avoid rate limiting.
 */
export async function getGitHubRepo(
  owner: string,
  repo: string
): Promise<GitHubRepoResponse | null> {
  const cacheKey = `repo:${owner}/${repo}`;

  try {
    return await memoizedFetch(cacheKey, async () => {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

      try {
        const response = await fetch(
          `https://api.github.com/repos/${owner}/${repo}`,
          {
            headers: {
              'User-Agent': 'Terragrunt-Docs',
            },
            signal: controller.signal,
          }
        );

        if (!response.ok) {
          console.error(
            `Failed to fetch GitHub repo ${owner}/${repo}:`,
            response.status,
            await response.text()
          );
          return null;
        }

        return response.json();
      } finally {
        clearTimeout(timeoutId);
      }
    });
  } catch (error) {
    console.error(`Error fetching GitHub repo ${owner}/${repo}:`, error);
    return null;
  }
}

/**
 * Fetches the latest release for a GitHub repository with memoization.
 * Results are cached for 1 hour to avoid rate limiting.
 */
export async function getLatestRelease(
  owner: string,
  repo: string
): Promise<GitHubReleaseResponse | null> {
  const cacheKey = `release:${owner}/${repo}`;

  try {
    return await memoizedFetch(cacheKey, async () => {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

      try {
        const response = await fetch(
          `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
          {
            headers: {
              'User-Agent': 'Terragrunt-Docs',
            },
            signal: controller.signal,
          }
        );

        if (!response.ok) {
          console.error(
            `Failed to fetch latest release for ${owner}/${repo}:`,
            response.status,
            await response.text()
          );
          return null;
        }

        return response.json();
      } finally {
        clearTimeout(timeoutId);
      }
    });
  } catch (error) {
    console.error(`Error fetching latest release for ${owner}/${repo}:`, error);
    return null;
  }
}

/**
 * Fetches a specific release by tag with memoization.
 * Results are cached for 1 hour to avoid rate limiting.
 */
export async function getReleaseByTag(
  owner: string,
  repo: string,
  tag: string
): Promise<GitHubReleaseResponse | null> {
  const cacheKey = `release-tag:${owner}/${repo}/${tag}`;

  try {
    return await memoizedFetch(cacheKey, async () => {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

      try {
        const response = await fetch(
          `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`,
          {
            headers: {
              'User-Agent': 'Terragrunt-Docs',
            },
            signal: controller.signal,
          }
        );

        if (!response.ok) {
          if (response.status !== 404) {
            console.error(
              `Failed to fetch release ${tag} for ${owner}/${repo}:`,
              response.status,
              await response.text()
            );
          }
          return null;
        }

        return response.json();
      } finally {
        clearTimeout(timeoutId);
      }
    });
  } catch (error) {
    console.error(`Error fetching release ${tag} for ${owner}/${repo}:`, error);
    return null;
  }
}

/**
 * Compares two refs and returns the commits in `head` that are not in `base`,
 * memoized for 1 hour to avoid rate limiting. Used to list the pull requests
 * merged since the latest release for versions that aren't published yet.
 *
 * The compare endpoint returns at most 250 commits; that's sufficient here
 * because releases are cut frequently.
 */
export async function compareCommits(
  owner: string,
  repo: string,
  base: string,
  head: string
): Promise<GitHubCommit[] | null> {
  const cacheKey = `compare:${owner}/${repo}/${base}...${head}`;

  try {
    return await memoizedFetch(cacheKey, async () => {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

      try {
        const response = await fetch(
          `https://api.github.com/repos/${owner}/${repo}/compare/${base}...${head}`,
          {
            headers: {
              'User-Agent': 'Terragrunt-Docs',
            },
            signal: controller.signal,
          }
        );

        if (!response.ok) {
          if (response.status !== 404) {
            console.error(
              `Failed to compare ${base}...${head} for ${owner}/${repo}:`,
              response.status,
              await response.text()
            );
          }
          return null;
        }

        const data = (await response.json()) as GitHubCompareResponse;
        return data.commits;
      } finally {
        clearTimeout(timeoutId);
      }
    });
  } catch (error) {
    console.error(
      `Error comparing ${base}...${head} for ${owner}/${repo}:`,
      error
    );
    return null;
  }
}

/**
 * Formats a star count for display (e.g., 8600 -> "8.6k")
 */
export function formatStarCount(stars: number): string {
  return (stars / 1000).toFixed(1) + 'k';
}
