// Shared Jira Data Center REST API helpers
// Centralizes HTTP calls for Jira issue operations.
// Follows the same promise-based pattern as atlasApi.js and ivantiApi.js.
//
// =========================================================================
// Charter Jira REST API Compliance
// =========================================================================
// Authentication:
//   - Service accounts use Basic Auth (required for shared integrations).
//   - PATs require ATLSUP approval and naming convention:
//     Function - Team - Approved ATLSUP ticket
//   - SSO must NOT be used for REST API integrations.
//
// Rate limiting (Charter-posted):
//   - 1 440 requests/day max
//   - Burst cap of 60 requests/minute (accumulates 1 req/idle minute)
//   - 429 response when limits are hit server-side
//
// Automation delays (Charter requirement):
//   - 1 second delay between GET requests
//   - 2 second delay between PUT, POST, or DELETE requests
//
// Forbidden patterns:
//   - /rest/api/2/field — must specify fields explicitly in every call
//   - /rest/api/2/issue/bulk — bulk updates are not allowed
//   - Single-issue GET loops — use bulk JQL search instead
//
// Required patterns:
//   - All GET requests MUST include a ?fields= parameter
//   - JQL MUST include at least one of: project+updated, assignee+updated,
//     status+updated
//   - JQL should use &updated>=-Xh to only fetch changed issues
//   - maxResults=1000 for search queries
//   - Issues must be updated one at a time (no bulk PUT)
// =========================================================================

const https = require('https');
const http = require('http');

// ---------------------------------------------------------------------------
// Configuration — read from process.env at module load
// ---------------------------------------------------------------------------
const JIRA_BASE_URL = process.env.JIRA_BASE_URL || '';
const JIRA_AUTH_METHOD = (process.env.JIRA_AUTH_METHOD || 'basic').toLowerCase();
const JIRA_API_USER = process.env.JIRA_API_USER || '';
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN || '';
const JIRA_PAT = process.env.JIRA_PAT || '';
const JIRA_SKIP_TLS = process.env.JIRA_SKIP_TLS === 'true';
const JIRA_PROJECT_KEY = process.env.JIRA_PROJECT_KEY || '';
const JIRA_ISSUE_TYPE = process.env.JIRA_ISSUE_TYPE || 'Task';

const requiredVars = JIRA_AUTH_METHOD === 'pat'
    ? ['JIRA_BASE_URL', 'JIRA_PAT']
    : ['JIRA_BASE_URL', 'JIRA_API_USER', 'JIRA_API_TOKEN'];

const missingVars = requiredVars.filter((v) => !process.env[v]);
if (missingVars.length > 0) {
    console.warn(`[jira-api] WARNING: Missing required environment variables: ${missingVars.join(', ')}. Jira API calls will fail.`);
}

const isConfigured = missingVars.length === 0;

// ---------------------------------------------------------------------------
// Default fields — every GET must specify fields explicitly.
// /rest/api/2/field is forbidden; we define the field list here.
// ---------------------------------------------------------------------------
const DEFAULT_FIELDS = [
    'summary', 'status', 'assignee', 'created', 'updated',
    'priority', 'issuetype', 'project', 'resolution'
];

// ---------------------------------------------------------------------------
// Rate limiter — enforces Charter's posted limits
// 1 440 events/day, burst of 60 events/minute
// ---------------------------------------------------------------------------
const DAILY_LIMIT = 1440;
const BURST_LIMIT = 60;
const MINUTE_MS = 60 * 1000;
const DAY_MS = 24 * 60 * 60 * 1000;

let dailyLog = [];
let minuteLog = [];

function pruneLog(log, windowMs) {
    const cutoff = Date.now() - windowMs;
    while (log.length > 0 && log[0] < cutoff) {
        log.shift();
    }
}

function checkRateLimit() {
    pruneLog(dailyLog, DAY_MS);
    pruneLog(minuteLog, MINUTE_MS);

    if (dailyLog.length >= DAILY_LIMIT) {
        return { allowed: false, reason: `Daily Jira API limit reached (${DAILY_LIMIT}/day). Resets at midnight.` };
    }
    if (minuteLog.length >= BURST_LIMIT) {
        return { allowed: false, reason: `Burst Jira API limit reached (${BURST_LIMIT}/min). Wait and retry.` };
    }
    return { allowed: true };
}

function recordRequest() {
    const now = Date.now();
    dailyLog.push(now);
    minuteLog.push(now);
}

/**
 * Return current rate limit usage for diagnostics.
 */
function getRateLimitStatus() {
    pruneLog(dailyLog, DAY_MS);
    pruneLog(minuteLog, MINUTE_MS);
    return {
        daily: { used: dailyLog.length, limit: DAILY_LIMIT, remaining: DAILY_LIMIT - dailyLog.length },
        burst: { used: minuteLog.length, limit: BURST_LIMIT, remaining: BURST_LIMIT - minuteLog.length }
    };
}

// ---------------------------------------------------------------------------
// Inter-request delay — Charter automation requirements
// 1s between GETs, 2s between PUT/POST/DELETE
// ---------------------------------------------------------------------------
const GET_DELAY_MS = 1000;
const WRITE_DELAY_MS = 2000;

let lastRequestTime = 0;
let lastRequestMethod = '';

/**
 * Wait the required delay before issuing the next request.
 * GET → 1s, PUT/POST/DELETE → 2s since the previous request.
 */
function waitForDelay(method) {
    const now = Date.now();
    const requiredDelay = (lastRequestMethod === 'GET') ? GET_DELAY_MS
        : (lastRequestMethod !== '') ? WRITE_DELAY_MS : 0;
    const elapsed = now - lastRequestTime;
    const remaining = requiredDelay - elapsed;

    if (remaining > 0) {
        return new Promise(resolve => setTimeout(resolve, remaining));
    }
    return Promise.resolve();
}

// ---------------------------------------------------------------------------
// Blocked endpoint guard
// ---------------------------------------------------------------------------
const BLOCKED_PATHS = [
    '/rest/api/2/field',       // Must specify fields in call, not query field list
    '/rest/api/2/issue/bulk',  // Bulk updates are not allowed
];

function isBlockedPath(urlPath) {
    return BLOCKED_PATHS.some(blocked => urlPath.startsWith(blocked));
}

// ---------------------------------------------------------------------------
// Generic request — supports GET, POST, PUT, DELETE
// Enforces rate limits, inter-request delays, and blocked-path guards.
// ---------------------------------------------------------------------------
async function jiraRequest(method, urlPath, body, options) {
    // Block forbidden endpoints
    if (isBlockedPath(urlPath)) {
        return Promise.reject(new Error(`Blocked: ${urlPath} is not allowed per Charter Jira API policy.`));
    }

    const limit = checkRateLimit();
    if (!limit.allowed) {
        return Promise.reject(new Error(limit.reason));
    }

    // Enforce inter-request delay
    await waitForDelay(method);

    const timeout = (options && options.timeout) || 15000;
    const fullUrl = new URL(JIRA_BASE_URL + urlPath);
    const isHttps = fullUrl.protocol === 'https:';
    const transport = isHttps ? https : http;

    const headers = {
        'accept': 'application/json'
    };

    // Auth header
    if (JIRA_AUTH_METHOD === 'pat') {
        headers['authorization'] = 'Bearer ' + JIRA_PAT;
    } else {
        const authString = Buffer.from(JIRA_API_USER + ':' + JIRA_API_TOKEN).toString('base64');
        headers['authorization'] = 'Basic ' + authString;
    }

    let bodyStr = null;
    if (body !== null && body !== undefined) {
        bodyStr = JSON.stringify(body);
        headers['content-type'] = 'application/json';
        headers['content-length'] = Buffer.byteLength(bodyStr);
    }

    recordRequest();
    lastRequestTime = Date.now();
    lastRequestMethod = method;

    return new Promise((resolve, reject) => {
        const reqOptions = {
            hostname: fullUrl.hostname,
            port: fullUrl.port || (isHttps ? 443 : 80),
            path: fullUrl.pathname + fullUrl.search,
            method: method,
            headers: headers,
            timeout: timeout
        };

        if (isHttps) {
            reqOptions.rejectUnauthorized = !JIRA_SKIP_TLS;
        }

        const req = transport.request(reqOptions, (res) => {
            let data = '';
            res.on('data', (chunk) => { data += chunk; });
            res.on('end', () => {
                if (res.statusCode === 429) {
                    resolve({ status: 429, body: data, rateLimited: true });
                } else {
                    resolve({ status: res.statusCode, body: data });
                }
            });
        });

        req.on('timeout', () => req.destroy(new Error(method + ' ' + urlPath + ' timed out')));
        req.on('error', (err) => {
            reject(new Error(method + ' ' + urlPath + ' failed: ' + err.message));
        });

        if (bodyStr) {
            req.write(bodyStr);
        }
        req.end();
    });
}

// ---------------------------------------------------------------------------
// Convenience wrappers
// ---------------------------------------------------------------------------
function jiraGet(urlPath, options) {
    return jiraRequest('GET', urlPath, null, options);
}

function jiraPost(urlPath, body, options) {
    return jiraRequest('POST', urlPath, body, options);
}

function jiraPut(urlPath, body, options) {
    return jiraRequest('PUT', urlPath, body, options);
}

function jiraDelete(urlPath, options) {
    return jiraRequest('DELETE', urlPath, null, options);
}

// ---------------------------------------------------------------------------
// High-level Jira operations — all comply with Charter requirements
// ---------------------------------------------------------------------------

/**
 * Fetch a single issue by key using a GET with explicit ?fields= parameter.
 * Charter requires all GETs to specify fields — /rest/api/2/field is forbidden.
 *
 * NOTE: For syncing multiple tickets, prefer searchIssuesByKeys() which uses
 * a single bulk JQL search instead of one GET per issue.
 *
 * @param {string} issueKey - e.g. "VULN-123"
 * @param {string[]} [fields] - Jira field names to return
 */
async function getIssue(issueKey, fields) {
    // Use JQL search to look up a single issue by key.
    // Issue keys are globally unique in Jira — no project filter needed.
    // Charter compliance: uses GET /rest/api/2/search with explicit field list.
    const jql = `key = "${issueKey}"`;
    const result = await searchIssues(jql, { fields: fields || DEFAULT_FIELDS, maxResults: 1, startAt: 0 });
    if (result.ok && result.data.issues && result.data.issues.length > 0) {
        return { ok: true, data: result.data.issues[0] };
    }
    if (result.ok && (!result.data.issues || result.data.issues.length === 0)) {
        return { ok: false, status: 404, body: 'Issue not found' };
    }
    return result;
}

/**
 * Bulk-fetch issues by their keys using a single JQL search.
 * This is the Charter-compliant way to sync multiple tickets — avoids
 * querying one issue at a time.
 *
 * @param {string[]} issueKeys - Array of Jira issue keys
 * @param {object} [opts] - { fields, maxResults }
 */
async function searchIssuesByKeys(issueKeys, opts) {
    if (!issueKeys || issueKeys.length === 0) {
        return { ok: true, data: { total: 0, issues: [] } };
    }

    // Build JQL: key in (KEY-1, KEY-2, ...) — issue keys are globally unique,
    // so no project filter needed. Add updated clause for Charter compliance.
    const keyList = issueKeys.map(k => `"${k}"`).join(', ');
    const jql = `key in (${keyList}) AND updated >= -72h`;
    const fields = (opts && opts.fields) || DEFAULT_FIELDS;
    const maxResults = Math.min((opts && opts.maxResults) || 1000, 1000);

    return searchIssues(jql, { fields, maxResults, startAt: 0 });
}

/**
 * Search issues via JQL (POST to /rest/api/2/search).
 * Charter requirements enforced:
 *   - fields array is always specified (never omitted)
 *   - maxResults capped at 1000
 *
 * The caller is responsible for including an &updated clause in the JQL
 * for recurring/scheduled queries.
 *
 * @param {string} jql - JQL query string
 * @param {object} [opts] - { startAt, maxResults, fields }
 */
async function searchIssues(jql, opts) {
    const startAt = (opts && opts.startAt) || 0;
    const maxResults = Math.min((opts && opts.maxResults) || 1000, 1000);
    const fields = (opts && opts.fields) || DEFAULT_FIELDS;

    const fieldList = encodeURIComponent(fields.join(','));
    const encodedJql = encodeURIComponent(jql);
    const queryString = `?jql=${encodedJql}&fields=${fieldList}&maxResults=${maxResults}&startAt=${startAt}`;
    const res = await jiraGet('/rest/api/2/search' + queryString);
    if (res.status === 200) {
        return { ok: true, data: JSON.parse(res.body) };
    }
    return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
}

/**
 * Create a new Jira issue (POST, subject to 2s delay).
 * @param {object} fields - Jira issue fields object
 */
async function createIssue(fields) {
    const res = await jiraPost('/rest/api/2/issue', { fields });
    if (res.status === 201) {
        return { ok: true, data: JSON.parse(res.body) };
    }
    return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
}

/**
 * Update a single Jira issue (PUT, subject to 2s delay).
 * Charter forbids bulk updates — issues must be updated one at a time.
 * @param {string} issueKey
 * @param {object} fields - Fields to update
 */
async function updateIssue(issueKey, fields) {
    const res = await jiraPut(
        `/rest/api/2/issue/${encodeURIComponent(issueKey)}`,
        { fields }
    );
    // Jira returns 204 on successful update
    if (res.status === 204) {
        return { ok: true };
    }
    return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
}

/**
 * Add a comment to an existing issue (POST, subject to 2s delay).
 */
async function addComment(issueKey, commentBody) {
    const res = await jiraPost(
        `/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment`,
        { body: commentBody }
    );
    if (res.status === 201) {
        return { ok: true, data: JSON.parse(res.body) };
    }
    return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
}

/**
 * Transition an issue to a new status (POST, subject to 2s delay).
 * @param {string} issueKey
 * @param {string} transitionId
 */
async function transitionIssue(issueKey, transitionId) {
    const res = await jiraPost(
        `/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`,
        { transition: { id: transitionId } }
    );
    if (res.status === 204) {
        return { ok: true };
    }
    return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
}

/**
 * Get available transitions for an issue.
 * Uses GET with explicit fields parameter (transitions endpoint returns
 * transitions by default, but we include the query param for compliance).
 */
async function getTransitions(issueKey) {
    const res = await jiraGet(
        `/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`
    );
    if (res.status === 200) {
        return { ok: true, data: JSON.parse(res.body) };
    }
    return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
}

/**
 * Test connectivity — calls /rest/api/2/myself to verify credentials.
 * This is a lightweight GET that returns the authenticated user.
 */
async function testConnection() {
    try {
        const res = await jiraGet('/rest/api/2/myself');
        if (res.status === 200) {
            const user = JSON.parse(res.body);
            return { ok: true, user: { name: user.name, displayName: user.displayName, emailAddress: user.emailAddress } };
        }
        return { ok: false, status: res.status, body: res.body };
    } catch (err) {
        return { ok: false, error: err.message };
    }
}

module.exports = {
    isConfigured,
    jiraRequest,
    jiraGet,
    jiraPost,
    jiraPut,
    jiraDelete,
    getIssue,
    searchIssuesByKeys,
    searchIssues,
    createIssue,
    updateIssue,
    addComment,
    transitionIssue,
    getTransitions,
    testConnection,
    getRateLimitStatus,
    DEFAULT_FIELDS,
    JIRA_PROJECT_KEY,
    JIRA_ISSUE_TYPE
};
