#!/usr/bin/env node

/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { execSync } from 'node:child_process';
import {
  mkdirSync,
  rmSync,
  readFileSync,
  existsSync,
  lstatSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const ACTIONLINT_VERSION = '1.7.7';
const SHELLCHECK_VERSION = '0.11.0';
const YAMLLINT_VERSION = '1.35.1';

const TEMP_DIR =
  process.env.GEMINI_LINT_TEMP_DIR || join(tmpdir(), 'gemini-cli-linters');

function getPlatformArch() {
  const platform = process.platform;
  const arch = process.arch;
  if (platform === 'linux' && arch === 'x64') {
    return {
      actionlint: 'linux_amd64',
      shellcheck: 'linux.x86_64',
    };
  }
  if (platform === 'darwin' && arch === 'x64') {
    return {
      actionlint: 'darwin_amd64',
      shellcheck: 'darwin.x86_64',
    };
  }
  if (platform === 'darwin' && arch === 'arm64') {
    return {
      actionlint: 'darwin_arm64',
      shellcheck: 'darwin.aarch64',
    };
  }
  if (platform === 'win32' && arch === 'x64') {
    return {
      actionlint: 'windows_amd64',
      // shellcheck is not used for Windows since it uses the .zip release
      // which has a consistent name across architectures
    };
  }
  throw new Error(`Unsupported platform/architecture: ${platform}/${arch}`);
}

const platformArch = getPlatformArch();

const PYTHON_VENV_PATH = join(TEMP_DIR, 'python_venv');

const pythonVenvPythonPath = join(
  PYTHON_VENV_PATH,
  process.platform === 'win32' ? 'Scripts' : 'bin',
  process.platform === 'win32' ? 'python.exe' : 'python',
);

const isWindows = process.platform === 'win32';

const actionlintCheck = isWindows
  ? `where actionlint 2>nul`
  : 'command -v actionlint';

const actionlintInstaller = isWindows
  ? `powershell -Command "` +
    `New-Item -ItemType Directory -Force -Path '${TEMP_DIR}/actionlint' | Out-Null; ` +
    `Invoke-WebRequest -Uri 'https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.zip' -OutFile '${TEMP_DIR}/.actionlint.zip'; ` +
    `Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
    `[System.IO.Compression.ZipFile]::ExtractToDirectory('${TEMP_DIR}/.actionlint.zip', '${TEMP_DIR}/actionlint')"`
  : `
      mkdir -p "${TEMP_DIR}/actionlint"
      curl -sSLo "${TEMP_DIR}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.tar.gz"
      tar -xzf "${TEMP_DIR}/.actionlint.tgz" -C "${TEMP_DIR}/actionlint"
    `;

const shellcheckCheck = isWindows
  ? `where shellcheck 2>nul`
  : 'command -v shellcheck';

const shellcheckInstaller = isWindows
  ? `powershell -Command "` +
    `Invoke-WebRequest -Uri 'https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.zip' -OutFile '${TEMP_DIR}/.shellcheck.zip'; ` +
    `Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
    `[System.IO.Compression.ZipFile]::ExtractToDirectory('${TEMP_DIR}/.shellcheck.zip', '${TEMP_DIR}/shellcheck')"`
  : `
      mkdir -p "${TEMP_DIR}/shellcheck"
      curl -sSLo "${TEMP_DIR}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.${platformArch.shellcheck}.tar.xz"
      tar -xf "${TEMP_DIR}/.shellcheck.txz" -C "${TEMP_DIR}/shellcheck" --strip-components=1
    `;

const yamllintCheck = isWindows
  ? `if exist "${PYTHON_VENV_PATH}\\Scripts\\yamllint.exe" (exit 0) else (exit 1)`
  : `test -x "${PYTHON_VENV_PATH}/bin/yamllint"`;

const yamllintInstaller = isWindows
  ? `python -m venv "${PYTHON_VENV_PATH}" && ` +
    `"${pythonVenvPythonPath}" -m pip install --upgrade pip && ` +
    `"${pythonVenvPythonPath}" -m pip install "yamllint==${YAMLLINT_VERSION}" --index-url https://pypi.org/simple`
  : `
    python3 -m venv "${PYTHON_VENV_PATH}" && \
    "${pythonVenvPythonPath}" -m pip install --upgrade pip && \
    "${pythonVenvPythonPath}" -m pip install "yamllint==${YAMLLINT_VERSION}" --index-url https://pypi.org/simple
  `;

/**
 * @typedef {{
 *   check: string;
 *   installer: string;
 *   run: string;
 * }}
 */

/**
 * @type {{[linterName: string]: Linter}}
 */
const LINTERS = {
  actionlint: {
    check: actionlintCheck,
    installer: actionlintInstaller,
    run: `
      actionlint \
        -color \
        -ignore 'SC2002:' \
        -ignore 'SC2016:' \
        -ignore 'SC2129:' \
        -ignore 'label ".+" is unknown'
    `,
  },
  shellcheck: {
    check: shellcheckCheck,
    installer: shellcheckInstaller,
    run: `
      git ls-files | grep -E '^([^.]+|.*\\.(sh|zsh|bash))' | xargs file --mime-type \
        | grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
        | xargs shellcheck \
          --check-sourced \
          --enable=all \
          --exclude=SC2002,SC2129,SC2310 \
          --severity=style \
          --format=gcc \
          --color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
    `,
  },
  yamllint: {
    check: yamllintCheck,
    installer: yamllintInstaller,
    run: "git ls-files | grep -E '\\.(yaml|yml)' | xargs yamllint --format github",
  },
};

function runCommand(command, stdio = 'inherit') {
  try {
    const env = { ...process.env };
    const nodeBin = join(process.cwd(), 'node_modules', '.bin');
    const sep = isWindows ? ';' : ':';
    const pythonBin = isWindows
      ? join(PYTHON_VENV_PATH, 'Scripts')
      : join(PYTHON_VENV_PATH, 'bin');
    // Windows sometimes uses 'Path' instead of 'PATH'
    const pathKey = 'Path' in env ? 'Path' : 'PATH';
    env[pathKey] = [
      nodeBin,
      join(TEMP_DIR, 'actionlint'),
      join(TEMP_DIR, 'shellcheck'),
      pythonBin,
      env[pathKey],
    ].join(sep);
    execSync(command, { stdio, env, shell: true });
    return true;
  } catch {
    return false;
  }
}

export function setupLinters() {
  console.log('Setting up linters...');
  if (!process.env.GEMINI_LINT_TEMP_DIR) {
    rmSync(TEMP_DIR, { recursive: true, force: true });
  }
  mkdirSync(TEMP_DIR, { recursive: true });

  for (const linter in LINTERS) {
    const { check, installer } = LINTERS[linter];
    if (!runCommand(check, 'ignore')) {
      console.log(`Installing ${linter}...`);
      if (!runCommand(installer)) {
        console.error(
          `Failed to install ${linter}. Please install it manually.`,
        );
        process.exit(1);
      }
    }
  }
  console.log('All required linters are available.');
}

export function runESLint() {
  console.log('\nRunning ESLint...');
  if (!runCommand('npm run lint')) {
    process.exit(1);
  }
}

export function runActionlint() {
  console.log('\nRunning actionlint...');
  if (!runCommand(LINTERS.actionlint.run)) {
    process.exit(1);
  }
}

export function runShellcheck() {
  console.log('\nRunning shellcheck...');
  if (!runCommand(LINTERS.shellcheck.run)) {
    process.exit(1);
  }
}

export function runYamllint() {
  console.log('\nRunning yamllint...');
  if (!runCommand(LINTERS.yamllint.run)) {
    process.exit(1);
  }
}

export function runPrettier() {
  console.log('\nRunning Prettier...');
  if (!runCommand('prettier --check .')) {
    console.log(
      'Prettier check failed. Please run "npm run format" to fix formatting issues.',
    );
    process.exit(1);
  }
}

export function runSensitiveKeywordLinter() {
  console.log('\nRunning sensitive keyword linter...');
  const SENSITIVE_PATTERN = /gemini-\d+(\.\d+)?/g;
  const ALLOWED_KEYWORDS = new Set([
    'gemini-3.1',
    'gemini-3',
    'gemini-3.0',
    'gemini-2.5',
    'gemini-2.0',
    'gemini-1.5',
    'gemini-1.0',
  ]);

  function getChangedFiles() {
    const baseRef = process.env.GITHUB_BASE_REF || 'main';
    try {
      execSync(`git fetch origin ${baseRef}`);
      const mergeBase = execSync(`git merge-base HEAD origin/${baseRef}`)
        .toString()
        .trim();
      return execSync(`git diff --name-only ${mergeBase}..HEAD`)
        .toString()
        .trim()
        .split('\n')
        .filter(Boolean);
    } catch {
      console.error(`Could not get changed files against origin/${baseRef}.`);
      try {
        console.log('Falling back to diff against HEAD~1');
        return execSync(`git diff --name-only HEAD~1..HEAD`)
          .toString()
          .trim()
          .split('\n')
          .filter(Boolean);
      } catch {
        console.error('Could not get changed files against HEAD~1 either.');
        process.exit(1);
      }
    }
  }

  const changedFiles = getChangedFiles();
  let violationsFound = false;

  for (const file of changedFiles) {
    if (!existsSync(file) || lstatSync(file).isDirectory()) {
      continue;
    }
    const content = readFileSync(file, 'utf-8');
    const lines = content.split('\n');
    let match;
    while ((match = SENSITIVE_PATTERN.exec(content)) !== null) {
      const keyword = match[0];
      if (!ALLOWED_KEYWORDS.has(keyword)) {
        violationsFound = true;
        const matchIndex = match.index;
        let lineNum = 0;
        let charCount = 0;
        for (let i = 0; i < lines.length; i++) {
          const line = lines[i];
          if (charCount + line.length + 1 > matchIndex) {
            lineNum = i + 1;
            const colNum = matchIndex - charCount + 1;
            console.log(
              `::warning file=${file},line=${lineNum},col=${colNum}::Found sensitive keyword "${keyword}". Please make sure this change is appropriate to submit.`,
            );
            break;
          }
          charCount += line.length + 1; // +1 for the newline
        }
      }
    }
  }

  if (!violationsFound) {
    console.log('No sensitive keyword violations found.');
  }
}

function stripJSONComments(json) {
  return json.replace(
    /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
    (m, g) => (g ? '' : m),
  );
}

export function runTSConfigLinter() {
  console.log('\nRunning tsconfig linter...');

  let files = [];
  try {
    // Find all tsconfig.json files under packages/ using a git pathspec
    files = execSync("git ls-files 'packages/**/tsconfig.json'")
      .toString()
      .trim()
      .split('\n')
      .filter(Boolean);
  } catch (e) {
    console.error('Error finding tsconfig.json files:', e.message);
    process.exit(1);
  }

  let hasError = false;

  for (const file of files) {
    const tsconfigPath = join(process.cwd(), file);
    if (!existsSync(tsconfigPath)) {
      console.error(`Error: ${tsconfigPath} does not exist.`);
      hasError = true;
      continue;
    }

    try {
      const content = readFileSync(tsconfigPath, 'utf-8');
      const config = JSON.parse(stripJSONComments(content));

      // Check if exclude exists and matches exactly
      if (config.exclude) {
        if (!Array.isArray(config.exclude)) {
          console.error(
            `Error: ${file} "exclude" must be an array. Found: ${JSON.stringify(
              config.exclude,
            )}`,
          );
          hasError = true;
        } else {
          const allowedExclude = new Set(['node_modules', 'dist']);
          const invalidExcludes = config.exclude.filter(
            (item) => !allowedExclude.has(item),
          );

          if (invalidExcludes.length > 0) {
            console.error(
              `Error: ${file} "exclude" contains invalid items: ${JSON.stringify(
                invalidExcludes,
              )}. Only "node_modules" and "dist" are allowed.`,
            );
            hasError = true;
          }
        }
      }
    } catch (error) {
      console.error(`Error parsing ${tsconfigPath}: ${error.message}`);
      hasError = true;
    }
  }

  if (hasError) {
    process.exit(1);
  }
}

export function runGithubActionsPinningLinter() {
  console.log('\nRunning GitHub Actions pinning linter...');

  let files = [];
  try {
    files = execSync(
      "git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' '.github/actions/**/*.yml' '.github/actions/**/*.yaml'",
    )
      .toString()
      .trim()
      .split('\n')
      .filter(Boolean);
  } catch (e) {
    console.error('Error finding GitHub Actions workflow files:', e.message);
    process.exit(1);
  }

  let violationsFound = false;
  // Improved regex to capture action name and ref, handling optional quotes and comments.
  const USES_PATTERN = /uses:\s*['"]?([^@\s'"]+)@([^#\s'"]+)['"]?/;
  const SHA_PATTERN = /^[0-9a-f]{40}$/i;

  for (const file of files) {
    if (!existsSync(file) || lstatSync(file).isDirectory()) {
      continue;
    }
    const content = readFileSync(file, 'utf-8');
    const lines = content.split('\n');

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      const match = line.match(USES_PATTERN);
      if (match) {
        const action = match[1];
        let ref = match[2];

        // Clean up any trailing quotes that might have been captured
        ref = ref.replace(/['"]$/, '');

        // Skip local actions (starting with ./), docker actions, and explicit exclusions
        if (
          action.startsWith('./') ||
          action.startsWith('docker://') ||
          line.includes('# github-actions-pinning:ignore')
        ) {
          continue;
        }

        if (!SHA_PATTERN.test(ref)) {
          violationsFound = true;
          const lineNum = i + 1;
          console.error(
            `::error file=${file},line=${lineNum}::Action "${action}" uses "${ref}" instead of a 40-character SHA.`,
          );
        }
      }
    }
  }

  if (violationsFound) {
    console.error(`
GitHub Actions pinning violations found. Please use exact commit hashes.

To automatically fix these, you can use the "ratchet" tool (https://github.com/sethvargo/ratchet):
  - Mac/Linux (Homebrew): brew install ratchet && ratchet pin .github/workflows/*.yml .github/actions/**/*.yml
  - Other platforms: Download from GitHub releases and run "ratchet pin .github/workflows/*.yml .github/actions/**/*.yml"

If you must use a tag, you can ignore this check by adding a comment (discouraged):
  uses: some-action@v1 # github-actions-pinning:ignore
`);
    process.exit(1);
  } else {
    console.log('No GitHub Actions pinning violations found.');
  }
}

function main() {
  const args = process.argv.slice(2);

  if (args.includes('--setup')) {
    setupLinters();
  }
  if (args.includes('--eslint')) {
    runESLint();
  }
  if (args.includes('--actionlint')) {
    runActionlint();
  }
  if (args.includes('--shellcheck')) {
    runShellcheck();
  }
  if (args.includes('--yamllint')) {
    runYamllint();
  }
  if (args.includes('--prettier')) {
    runPrettier();
  }
  if (args.includes('--sensitive-keywords')) {
    runSensitiveKeywordLinter();
  }
  if (args.includes('--tsconfig')) {
    runTSConfigLinter();
  }
  if (args.includes('--check-github-actions-pinning')) {
    runGithubActionsPinningLinter();
  }

  if (args.length === 0) {
    setupLinters();
    runESLint();
    runActionlint();
    runShellcheck();
    runYamllint();
    runPrettier();
    runSensitiveKeywordLinter();
    runTSConfigLinter();
    runGithubActionsPinningLinter();
    console.log('\nAll linting checks passed!');
  }
}

main();
