// @ts-check

/**
 * @typedef {object} ReleaseCycle
 * @property {string|number} cycle - Release cycle (e.g. "1.20", 8.1, etc.)
 * @property {string} [releaseDate] - YYYY-MM-DD string for the first release in this cycle
 * @property {string|boolean} [eol] - End of Life date (YYYY-MM-DD) or false if not EoL
 * @property {string} [latest] - Latest release in this cycle
 * @property {string|null} [link] - Link to changelog or similar, if available
 * @property {boolean|string} [lts] - Whether this cycle is non-LTS (false), or LTS starting on a given date
 * @property {string|boolean} [support] - Active support date (YYYY-MM-DD) or boolean
 * @property {string|boolean} [discontinued] - Discontinued date (YYYY-MM-DD) or boolean
 */

/**
 * @typedef {object} EolNewReleaseConfig
 * @property {string} languageName
 * @property {string} eolJsonUrl
 * @property {string} eolViewUrl
 * @property {number} eolLookbackDays
 * @property {number} newReleaseThresholdDays
 * @property {boolean} ltsOnly
 * @property {number} retryCount
 * @property {number} retryIntervalSec
 */

/**
 * This script checks EoL and new releases from endoflife.date JSON.
 * It creates Issues for:
 *   - EoL reached within eolLookbackDays
 *   - New releases within newReleaseThresholdDays
 * If fetching fails after multiple retries, an error Issue is created once per week.
 *
 * Note this script is used in a GitHub Action workflow, and some line/line-bot-* repositories.
 * If you modify this script, please consider syncing the changes to other repositories.
 *
 * @param {import('@actions/github-script').AsyncFunctionArguments} actionCtx
 * @param {EolNewReleaseConfig} config
 */
module.exports = async function checkEolAndNewReleases(actionCtx, config) {
  const { github, context, core } = actionCtx;
  const {
    languageName,
    eolJsonUrl,
    eolViewUrl,
    eolLookbackDays,
    newReleaseThresholdDays,
    ltsOnly,
    retryCount,
    retryIntervalSec,
  } = config;

  /**
   * Returns a simple "year-week" string like "2025-W09".
   * This is a rough calculation (not strictly ISO-8601).
   * @param {Date} date
   * @returns {string}
   */
  const getYearWeek = (date) => {
    const startOfYear = new Date(date.getFullYear(), 0, 1);
    const dayOfYear = Math.floor((date - startOfYear) / 86400000) + 1;
    const weekNum = Math.ceil(dayOfYear / 7);
    return `${date.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
  };

  /**
   * Simple dedent function.
   * Removes common leading indentation based on the minimum indent across all lines.
   * Also trims empty lines at the start/end.
   * @param {string} str
   * @returns {string}
   */
  const dedent = (str) => {
    const lines = str.split('\n');
    while (lines.length && lines[0].trim() === '') lines.shift();
    while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();

    /** @type {number[]} */
    const indents = lines
      .filter(line => line.trim() !== '')
      .map(line => (line.match(/^(\s+)/)?.[1].length) ?? 0);

    const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
    return lines.map(line => line.slice(minIndent)).join('\n');
  };

  /**
   * Creates an Issue if an Issue with the same title does not exist (state=all).
   * @param {string} title
   * @param {string} body
   * @param {string[]} [labels]
   * @returns {Promise<boolean>} true if created, false if an Issue with same title already exists
   */
  const createIssueIfNotExists = async (title, body, labels = []) => {
    const issues = await github.rest.issues.listForRepo({
      owner: context.repo.owner,
      repo: context.repo.repo,
      state: 'all',
      per_page: 100,
    });
    const found = issues.data.find(i => i.title === title);
    if (found) {
      core.info(`Issue already exists: "${title}"`);
      return false;
    }
    await github.rest.issues.create({
      owner: context.repo.owner,
      repo: context.repo.repo,
      title,
      body,
      labels,
    });
    core.notice(`Created Issue: "${title}"`);
    return true;
  };

  /**
   * Fetch with retry, returning an array of ReleaseCycle objects.
   * @param {string} url
   * @returns {Promise<ReleaseCycle[]>}
   */
  const fetchWithRetry = async (url) => {
    let lastErr = null;
    for (let i = 1; i <= retryCount; i++) {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP ${response.status} ${response.statusText}`);
        }
        /** @type {ReleaseCycle[]} */
        const jsonData = await response.json();
        return jsonData;
      } catch (err) {
        lastErr = err;
        core.warning(`Fetch failed (attempt ${i}/${retryCount}): ${err.message}`);
        if (i < retryCount) {
          await new Promise(r => setTimeout(r, retryIntervalSec * 1000));
        }
      }
    }
    throw new Error(`Failed to fetch after ${retryCount} attempts: ${lastErr?.message}`);
  };

  /**
   * Check EoL for a single release.
   * @param {ReleaseCycle} release
   * @param {Date} now
   * @param {Date} eolLookbackDate
   */
  const checkEoL = async (release, now, eolLookbackDate) => {
    if (ltsOnly && release.lts === false) {
      core.info(`Skipping non-LTS release: ${release.cycle}`);
      return;
    }
    if (typeof release.eol === 'string') {
      const eolDate = new Date(release.eol);
      if (!isNaN(eolDate.getTime())) {
        // Check if it reached EoL within the last eolLookbackDays
        if (eolDate <= now && eolDate >= eolLookbackDate) {
          if (!release.cycle) return;
          const title = `Drop ${languageName} ${release.cycle} support`;
          const body = dedent(`
                        This version(${languageName} ${release.cycle}) has reached End of Life.
                        Please drop its support as needed.

                        **EoL date**: ${release.eol}
                        endoflife.date for ${languageName}: ${eolViewUrl}
                        `);
          await createIssueIfNotExists(title, body, ['keep']);
        }
      }
    }
  };

  /**
   * Check new release for a single release.
   * @param {ReleaseCycle} release
   * @param {Date} now
   * @param {Date} newReleaseSince
   */
  const checkNewRelease = async (release, now, newReleaseSince) => {
    if (ltsOnly && release.lts === false) {
      core.info(`Skipping non-LTS release: ${release.cycle}`);
      return;
    }
    if (typeof release.releaseDate === 'string') {
      const rDate = new Date(release.releaseDate);
      if (!isNaN(rDate.getTime())) {
        // Check if releaseDate is within newReleaseThresholdDays
        if (rDate >= newReleaseSince && rDate <= now) {
          if (!release.cycle) return;
          const ltsTag = ltsOnly ? ' (LTS)' : '';
          const title = `Support ${languageName} ${release.cycle}${ltsTag}`;
          const body = dedent(`
                        A new version(${languageName} ${release.cycle}) has been released.
                        Please start to support it.

                        **Release date**: ${release.releaseDate}
                        endoflife.date for ${languageName}: ${eolViewUrl}
                        `);
          await createIssueIfNotExists(title, body, ['keep']);
        }
      }
    }
  };

  core.info(`Starting EoL & NewRelease check for ${languageName} ...`);
  const now = new Date();
  const eolLookbackDate = new Date(now);
  eolLookbackDate.setDate(eolLookbackDate.getDate() - eolLookbackDays);

  const newReleaseSince = new Date(now);
  newReleaseSince.setDate(newReleaseSince.getDate() - newReleaseThresholdDays);

  try {
    const data = await fetchWithRetry(eolJsonUrl);
    for (const release of data) {
      core.info(`Checking release: ${JSON.stringify(release)}`);
      await checkEoL(release, now, eolLookbackDate);
      await checkNewRelease(release, now, newReleaseSince);
    }
  } catch (err) {
    core.error(`Error checking EoL/NewReleases for ${languageName}: ${err.message}`);
    const yw = getYearWeek(new Date());
    const errorTitle = `[CI ERROR] EoL/NewRelease check for ${languageName} in ${yw}`;
    const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
    const body = dedent(`
            The automated check for EoL and new releases failed (retried ${retryCount} times).
            **Error**: ${err.message}
            **Action URL**: [View job log here](${runUrl})
            Please investigate (network issues, invalid JSON, etc.) and fix it to monitor EOL site automatically.
            `);
    await createIssueIfNotExists(errorTitle, body, ['keep']);
    core.setFailed(err.message);
  }
};
