#!/usr/bin/env node
const fs = require('fs');
const crypto = require('crypto');

const [command, ...argv] = process.argv.slice(2);

const parseArgs = (args) => {
  const parsed = {};
  let i = 0;
  while (i < args.length) {
    const arg = args[i];
    if (arg.startsWith('--')) {
      const key = arg.replace(/^--/, '');
      const next = args[i + 1];
      if (next && !next.startsWith('--')) {
        parsed[key] = next;
        i += 2;
      } else {
        parsed[key] = true;
        i += 1;
      }
    } else {
      parsed._ = parsed._ || [];
      parsed._.push(arg);
      i += 1;
    }
  }
  return parsed;
};

const args = parseArgs(argv);

const ensureEnv = (key, optional = false) => {
  const value = process.env[key];
  if (!value && !optional) {
    throw new Error(`Missing required env: ${key}`);
  }
  return value;
};

const readConfig = () => {
  const baseUrl = ensureEnv('REDMINE_URL');
  const url = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
  return {
    url,
    apiKey: ensureEnv('REDMINE_API_KEY'),
    projectId: ensureEnv('REDMINE_PROJECT_ID'),
    trackerBugId: ensureEnv('REDMINE_TRACKER_BUG_ID'),
    trackerSecurityId: process.env.REDMINE_TRACKER_SECURITY_ID,
    assigneeId: process.env.REDMINE_ASSIGNEE_ID,
  };
};

const redmineUrl = (config, path, params = {}) => {
  const url = new URL(path.replace(/^\//, ''), config.url);
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined && value !== null && value !== '') {
      url.searchParams.set(key, value);
    }
  });
  return url;
};

const fetchJson = async (config, path, options = {}, params = {}) => {
  const url = redmineUrl(config, path, params);
  const res = await fetch(url, {
    headers: {
      'X-Redmine-API-Key': config.apiKey,
      'Content-Type': 'application/json',
    },
    ...options,
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Redmine request failed (${res.status}): ${text}`);
  }

  if (res.status === 204) {
    return {};
  }

  return res.json();
};

const fetchAllOpenIssues = async (config) => {
  const issues = [];
  const limit = 100;
  let offset = 0;
  let total = null;

  while (total === null || offset < total) {
    const data = await fetchJson(
      config,
      '/issues.json',
      {},
      {
        project_id: config.projectId,
        status_id: 'open',
        limit,
        offset,
        sort: 'updated_on:desc',
      },
    );
    if (Array.isArray(data.issues)) {
      issues.push(...data.issues);
    }
    total = data.total_count ?? issues.length;
    offset += limit;
    if (!data.issues || data.issues.length === 0) {
      break;
    }
  }

  return issues;
};

const findExistingIssue = async (config, fingerprint) => {
  if (!fingerprint) return null;
  const issues = await fetchAllOpenIssues(config);
  return issues.find(
    (issue) =>
      issue.subject?.includes(fingerprint) ||
      issue.description?.includes(`Fingerprint: ${fingerprint}`),
  );
};

const computeFingerprint = (seed) => {
  const hash = crypto.createHash('sha1').update(seed).digest('hex');
  return hash.slice(0, 12);
};

const readFailures = (opts) => {
  const failures = [];
  if (opts['failures-file']) {
    try {
      const text = fs.readFileSync(opts['failures-file'], 'utf8');
      text
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter(Boolean)
        .forEach((line) => failures.push(line));
    } catch (err) {
      console.error(`Could not read failures file: ${err.message}`);
    }
  }

  if (opts.failures) {
    failures.push(opts.failures);
  }

  if (failures.length === 0) {
    failures.push('Test failure (no details provided)');
  }

  return failures;
};

const createIssue = async (config, payload) => {
  const body = {
    issue: {
      project_id: config.projectId,
      tracker_id: payload.trackerId,
      subject: payload.subject,
      description: payload.description,
    },
  };

  if (config.assigneeId) {
    body.issue.assigned_to_id = config.assigneeId;
  }

  const res = await fetchJson(config, '/issues.json', {
    method: 'POST',
    body: JSON.stringify(body),
  });
  return res.issue || res;
};

const handleCreateTestIssue = async () => {
  const config = readConfig();
  const suite = args.suite || 'tests';
  const tracker = (args.tracker || 'bug').toLowerCase();
  const trackerId =
    tracker === 'security'
      ? config.trackerSecurityId || config.trackerBugId
      : config.trackerBugId;

  if (!trackerId) {
    throw new Error('Missing tracker id. Set REDMINE_TRACKER_BUG_ID (and optionally REDMINE_TRACKER_SECURITY_ID).');
  }

  const failCount = Number(args['fail-count'] || args.failcount || 0);
  const failures = readFailures(args);
  const fingerprintSeed =
    args['fingerprint-seed'] ||
    `${suite}|${args.target || ''}|${failCount}|${failures.join('|')}`;
  const fingerprint = args.fingerprint || computeFingerprint(fingerprintSeed);
  const subject = `[${suite}] ${failCount || failures.length} failure${failCount === 1 ? '' : 's'} (${tracker}) [${fingerprint}]`;

  const descriptionLines = [
    `Suite: ${suite}`,
    args.run ? `Run: ${args.run}` : null,
    args.target ? `Target: ${args.target}` : null,
    `Tracker: ${tracker}`,
    `Failures (${failures.length}):`,
    ...failures.map((line) => `- ${line}`),
    args['summary-file'] ? `Summary: ${args['summary-file']}` : null,
    args['failures-file'] ? `Log: ${args['failures-file']}` : null,
    `Fingerprint: ${fingerprint}`,
  ].filter(Boolean);

  const existing = await findExistingIssue(config, fingerprint);
  if (existing) {
    console.log(
      `Open issue #${existing.id} already exists for fingerprint ${fingerprint}; not creating a duplicate.`,
    );
    return;
  }

  const created = await createIssue(config, {
    trackerId,
    subject,
    description: descriptionLines.join('\n'),
  });

  console.log(`Created Redmine issue #${created.id}: ${subject}`);
};

const handleListOpen = async () => {
  const config = readConfig();
  const issues = await fetchAllOpenIssues(config);
  if (!issues.length) {
    console.log('No open issues found for the configured project.');
    return;
  }

  const groups = issues.reduce((acc, issue) => {
    const tracker = issue.tracker?.name || 'Unknown';
    acc[tracker] = acc[tracker] || [];
    acc[tracker].push(issue);
    return acc;
  }, {});

  Object.entries(groups).forEach(([trackerName, trackerIssues]) => {
    console.log(`${trackerName} (${trackerIssues.length})`);
    trackerIssues.forEach((issue) => {
      const status = issue.status?.name ? ` [${issue.status.name}]` : '';
      const priority = issue.priority?.name ? ` (${issue.priority.name})` : '';
      console.log(`- #${issue.id}${status}${priority}: ${issue.subject}`);
    });
    console.log('');
  });
};

const printHelp = () => {
  console.log(`Usage:
  redmine-report.js create-test-issue --suite <name> --run <id> --fail-count <n> --failures-file <path> [--target <url>] [--tracker bug|security] [--fingerprint <id>]
  redmine-report.js list-open

Env:
  REDMINE_URL                   Base URL, e.g. https://redmine.example.com
  REDMINE_API_KEY               API key
  REDMINE_PROJECT_ID            Project to file against
  REDMINE_TRACKER_BUG_ID        Tracker id for bugs
  REDMINE_TRACKER_SECURITY_ID   Tracker id for security issues (optional)
  REDMINE_ASSIGNEE_ID           Default assignee (optional)
`);
};

const main = async () => {
  try {
    switch (command) {
      case 'create-test-issue':
        await handleCreateTestIssue();
        break;
      case 'list-open':
        await handleListOpen();
        break;
      case '-h':
      case '--help':
      case undefined:
        printHelp();
        process.exit(command ? 0 : 1);
        break;
      default:
        console.error(`Unknown command: ${command}`);
        printHelp();
        process.exit(1);
    }
  } catch (err) {
    console.error(`Redmine command failed: ${err.message}`);
    process.exit(1);
  }
};

main();
