/**
 * Jira API Client
 *
 * Supports:
 * 1. MCP Server (Atlassian Remote MCP) - Primary
 * 2. REST API v3 - Fallback
 *
 * Features:
 * - List issues with JQL filtering (by sprint, status, etc.)
 * - Create issues (Epic, Story, Subtask, Bug, Task)
 * - Update issues
 * - Get sprint information
 */
import { credentialsManager } from '../../core/credentials/credentials-manager.js';
import { consoleLogger } from '../../utils/logger.js';
/**
 * Module logger - can be replaced for testing
 */
let moduleLogger = consoleLogger;
/**
 * Set the logger for this module
 */
export function setJiraClientLogger(logger) {
    moduleLogger = logger;
}
export class JiraClient {
    constructor() {
        this.apiVersion = '3';
        this.credentials = credentialsManager.getJiraCredentials();
        // Normalize domain URL
        const domain = this.credentials.domain;
        if (domain.startsWith('http')) {
            this.baseUrl = domain;
        }
        else {
            this.baseUrl = `https://${domain}`;
        }
    }
    /**
     * Get authorization header for REST API
     */
    getAuthHeader() {
        const auth = Buffer.from(`${this.credentials.email}:${this.credentials.apiToken}`).toString('base64');
        return `Basic ${auth}`;
    }
    /**
     * Extract plain text from Atlassian Document Format (ADF)
     */
    extractTextFromAdf(body) {
        if (!body) {
            return '';
        }
        if (typeof body === 'string') {
            return body;
        }
        if (!body.content) {
            return '';
        }
        const textParts = [];
        for (const block of body.content) {
            if (block.type === 'paragraph' && block.content) {
                for (const inline of block.content) {
                    if (inline.type === 'text') {
                        textParts.push(inline.text);
                    }
                }
                textParts.push('\n');
            }
        }
        return textParts.join('').trim();
    }
    /**
     * Search issues using JQL
     */
    async searchIssues(filter = {}) {
        const jql = filter.jql || this.buildJqlQuery(filter);
        moduleLogger.info('🔍 Querying Jira with JQL:', jql);
        // Use the new /search/jql endpoint as per Jira Cloud API v3
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/search/jql`;
        const maxResults = filter.maxResults || 50;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                jql,
                maxResults,
                fields: ['*all']
            })
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        const data = await response.json();
        moduleLogger.info(`✅ Retrieved ${data.issues.length} issues from Jira`);
        return data.issues || [];
    }
    /**
     * Build JQL query from filter
     */
    buildJqlQuery(filter) {
        const conditions = [];
        if (filter.project) {
            conditions.push(`project = "${filter.project}"`);
        }
        if (filter.issueType && filter.issueType.length > 0) {
            const types = filter.issueType.map(t => `"${t}"`).join(', ');
            conditions.push(`issuetype IN (${types})`);
        }
        if (filter.status && filter.status.length > 0) {
            const statuses = filter.status.map(s => `"${s}"`).join(', ');
            conditions.push(`status IN (${statuses})`);
        }
        if (filter.sprint) {
            if (typeof filter.sprint === 'number') {
                conditions.push(`sprint = ${filter.sprint}`);
            }
            else if (filter.sprint === 'current') {
                conditions.push(`sprint in openSprints()`);
            }
            else {
                conditions.push(`sprint = "${filter.sprint}"`);
            }
        }
        if (filter.assignee) {
            conditions.push(`assignee = "${filter.assignee}"`);
        }
        if (filter.labels && filter.labels.length > 0) {
            filter.labels.forEach(label => {
                conditions.push(`labels = "${label}"`);
            });
        }
        // Pull sync filter: updated since timestamp
        if (filter.updatedSince) {
            // JIRA JQL uses relative date format: "2025-12-01 10:00" or "-1h"
            const hoursSince = Math.ceil((Date.now() - filter.updatedSince.getTime()) / 3600000);
            conditions.push(`updated >= -${hoursSince}h`);
        }
        // Pull sync filter: specific issue keys
        if (filter.issueKeys && filter.issueKeys.length > 0) {
            const keys = filter.issueKeys.map(k => `"${k}"`).join(', ');
            conditions.push(`key IN (${keys})`);
        }
        if (conditions.length === 0) {
            // Default: return recent issues
            return 'ORDER BY updated DESC';
        }
        const whereClause = conditions.join(' AND ');
        return `${whereClause} ORDER BY updated DESC`;
    }
    /**
     * Create a new issue
     */
    async createIssue(issue, projectKey) {
        moduleLogger.info(`🔨 Creating ${issue.issueType}: ${issue.summary}`);
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue`;
        // Build issue payload
        const payload = {
            fields: {
                project: {
                    key: projectKey
                },
                summary: issue.summary,
                issuetype: {
                    name: issue.issueType
                }
            }
        };
        // Add description (Jira uses Atlassian Document Format - ADF)
        if (issue.description) {
            payload.fields.description = textToAdf(issue.description);
        }
        // Add priority (skip if not available in project)
        if (issue.priority) {
            try {
                payload.fields.priority = {
                    name: issue.priority
                };
            }
            catch (error) {
                // Priority field might not be available in this project
                moduleLogger.warn('⚠️  Priority field not available, skipping');
            }
        }
        // Add labels
        if (issue.labels && issue.labels.length > 0) {
            payload.fields.labels = issue.labels;
        }
        // Link to parent (for Sub-tasks)
        if (issue.parentKey) {
            payload.fields.parent = {
                key: issue.parentKey
            };
        }
        // Link to Epic (for Stories)
        // Note: Epic Link field varies by Jira instance, typically customfield_10014
        if (issue.epicKey) {
            payload.fields.customfield_10014 = issue.epicKey;
        }
        // Add custom fields
        if (issue.customFields) {
            Object.entries(issue.customFields).forEach(([key, value]) => {
                payload.fields[key] = value;
            });
        }
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(payload)
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        const result = await response.json();
        moduleLogger.info(`✅ Created ${issue.issueType} ${result.key}: ${issue.summary}`);
        // Fetch full issue details
        return this.getIssue(result.key);
    }
    /**
     * Update an existing issue
     */
    async updateIssue(update) {
        moduleLogger.info(`🔧 Updating issue ${update.key}`);
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${update.key}`;
        // Build update payload
        const payload = {
            fields: {}
        };
        if (update.summary) {
            payload.fields.summary = update.summary;
        }
        if (update.description !== undefined) {
            payload.fields.description = textToAdf(update.description);
        }
        if (update.priority) {
            payload.fields.priority = {
                name: update.priority
            };
        }
        if (update.labels) {
            payload.fields.labels = update.labels;
        }
        // Add custom fields
        if (update.customFields) {
            Object.entries(update.customFields).forEach(([key, value]) => {
                payload.fields[key] = value;
            });
        }
        const response = await fetch(url, {
            method: 'PUT',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(payload)
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        moduleLogger.info(`✅ Updated issue ${update.key}`);
        // Transition status if specified
        if (update.status) {
            await this.transitionIssue(update.key, update.status);
        }
        // Fetch updated issue
        return this.getIssue(update.key);
    }
    /**
     * Transition issue to a new status
     */
    async transitionIssue(issueKey, targetStatus) {
        moduleLogger.info(`🔀 Transitioning ${issueKey} to ${targetStatus}`);
        // Get available transitions
        const transitionsUrl = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${issueKey}/transitions`;
        const transitionsResponse = await fetch(transitionsUrl, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!transitionsResponse.ok) {
            throw new Error(`Failed to get transitions for ${issueKey}`);
        }
        const transitionsData = await transitionsResponse.json();
        const transition = transitionsData.transitions.find((t) => t.to.name.toLowerCase() === targetStatus.toLowerCase());
        if (!transition) {
            moduleLogger.warn(`⚠️  No transition found to status "${targetStatus}" for ${issueKey}`);
            return;
        }
        // Execute transition
        const response = await fetch(transitionsUrl, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                transition: {
                    id: transition.id
                }
            })
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Failed to transition ${issueKey}: ${error}`);
        }
        moduleLogger.info(`✅ Transitioned ${issueKey} to ${targetStatus}`);
    }
    /**
     * Get issue by key
     */
    async getIssue(issueKey) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${issueKey}`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        return response.json();
    }
    /**
     * Get sprints for a board
     */
    async getSprints(boardId) {
        const url = `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        const data = await response.json();
        return data.values || [];
    }
    /**
     * Get active sprints
     */
    async getActiveSprints(boardId) {
        const sprints = await this.getSprints(boardId);
        return sprints.filter(s => s.state === 'active');
    }
    /**
     * Test connection to Jira
     */
    async testConnection() {
        try {
            const url = `${this.baseUrl}/rest/api/${this.apiVersion}/myself`;
            const response = await fetch(url, {
                headers: {
                    'Authorization': this.getAuthHeader(),
                    'Content-Type': 'application/json'
                }
            });
            if (response.ok) {
                const user = await response.json();
                moduleLogger.info(`✅ Jira connection successful (${user.displayName})`);
                return true;
            }
            else {
                const error = await response.text();
                moduleLogger.error(`❌ Jira connection failed (${response.status}): ${error}`);
                return false;
            }
        }
        catch (error) {
            moduleLogger.error('❌ Jira connection failed:', error);
            return false;
        }
    }
    /**
     * Get projects accessible to the user
     */
    async getProjects() {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/project`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        return response.json();
    }
    /**
     * Search projects with pagination (for three-tier loading)
     *
     * @param options Search options
     * @returns Paginated project results
     */
    async searchProjects(options = {}) {
        const { maxResults = 50, startAt = 0, orderBy = 'name' } = options;
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/project/search?maxResults=${maxResults}&startAt=${startAt}&orderBy=${orderBy}`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        return response.json();
    }
    /**
     * Get boards for a project
     *
     * @param projectKey Project key (e.g., "BACKEND")
     * @returns Boards array
     */
    async getBoards(projectKey) {
        const url = `${this.baseUrl}/rest/agile/1.0/board?projectKeyOrId=${projectKey}`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        return response.json();
    }
    /**
     * Get components for a project
     *
     * @param projectKey Project key (e.g., "BACKEND")
     * @returns Components array
     */
    async getComponents(projectKey) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/project/${projectKey}/components`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        return response.json();
    }
    /**
     * Get versions for a project
     *
     * @param projectKey Project key (e.g., "BACKEND")
     * @returns Versions array
     */
    async getVersions(projectKey) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/project/${projectKey}/versions`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        return response.json();
    }
    /**
     * Fetch recently changed issues for pull sync (Increment 0089)
     *
     * Queries JIRA for issues that have changed since a given timestamp.
     * Uses JQL `updated >=` filter and optionally requests changelog expansion.
     *
     * @param since - Timestamp to query changes from
     * @param linkedIssueKeys - Optional list of issue keys to filter (linked items only)
     * @returns Array of external changes with changedAt, changedBy, and current values
     */
    async fetchRecentChanges(since, linkedIssueKeys) {
        // Build filter for recently changed items
        const filter = {
            updatedSince: since,
            maxResults: 100, // Limit to prevent overwhelming results
        };
        // If specific keys provided, only query those
        if (linkedIssueKeys && linkedIssueKeys.length > 0) {
            filter.issueKeys = linkedIssueKeys;
        }
        // Query issues with expanded changelog
        const jql = this.buildJqlQuery(filter);
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/search/jql`;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                jql,
                maxResults: filter.maxResults,
                fields: ['summary', 'status', 'priority', 'assignee', 'updated', 'creator'],
                expand: ['changelog'] // Request changelog for field-level changes
            })
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        const data = await response.json();
        const issues = data.issues || [];
        // Map to ExternalChange format
        const changes = issues.map((issue) => {
            // Extract changelog items if available
            const changedFields = [];
            if (issue.changelog?.histories) {
                // Get the most recent history entries since 'since' timestamp
                for (const history of issue.changelog.histories) {
                    const historyDate = new Date(history.created);
                    if (historyDate >= since) {
                        for (const item of history.items || []) {
                            changedFields.push({
                                field: item.field,
                                oldValue: item.fromString ?? item.from,
                                newValue: item.toString ?? item.to,
                            });
                        }
                    }
                }
            }
            return {
                platform: 'jira',
                externalId: issue.key,
                issueKey: issue.key,
                changedAt: issue.fields?.updated || new Date().toISOString(),
                changedBy: issue.changelog?.histories?.[0]?.author?.displayName ||
                    issue.changelog?.histories?.[0]?.author?.emailAddress ||
                    issue.fields?.creator?.displayName ||
                    'unknown',
                changedFields,
                currentState: {
                    status: issue.fields?.status?.name || 'Unknown',
                    priority: issue.fields?.priority?.name || null,
                    assignee: issue.fields?.assignee?.displayName ||
                        issue.fields?.assignee?.emailAddress ||
                        null,
                },
            };
        });
        return changes;
    }
    /**
     * Get issue changelog for detailed change tracking
     *
     * Use this for detailed changelog when needed.
     * GET /rest/api/3/issue/{issueKey}/changelog
     *
     * @param issueKey - Issue key
     * @param maxResults - Limit number of changelog entries
     * @returns Array of changelog entries with field changes
     */
    async getIssueChangelog(issueKey, maxResults = 10) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${issueKey}/changelog?maxResults=${maxResults}`;
        const response = await fetch(url, {
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Jira API Error (${response.status}): ${error}`);
        }
        const data = await response.json();
        return data.values || [];
    }
    /**
     * Get last comment on JIRA issue (for idempotency check)
     *
     * GET /rest/api/3/issue/{issueIdOrKey}/comment?orderBy=-created&maxResults=1
     *
     * Returns the most recent comment body, or null if no comments exist.
     * Used to prevent posting duplicate progress comments.
     */
    async getLastComment(issueKey) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${issueKey}/comment?orderBy=-created&maxResults=1`;
        try {
            const response = await fetch(url, {
                method: 'GET',
                headers: {
                    'Authorization': this.getAuthHeader(),
                    'Accept': 'application/json'
                }
            });
            if (!response.ok) {
                // If error, return null (no comments or API error)
                return null;
            }
            const data = await response.json();
            const comments = data.comments || [];
            if (comments.length === 0) {
                return null;
            }
            const lastComment = comments[0];
            const bodyText = this.extractTextFromAdf(lastComment.body);
            return {
                body: bodyText,
                author: lastComment.author?.displayName || lastComment.author?.emailAddress || 'unknown'
            };
        }
        catch {
            // On any error, return null to allow the comment to proceed
            return null;
        }
    }
    /**
     * Add comment to JIRA issue (T-034C)
     *
     * POST /rest/api/3/issue/{issueIdOrKey}/comment
     */
    async addComment(issueKey, comment) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${issueKey}/comment`;
        // JIRA comment body uses Atlassian Document Format (ADF) or plain text
        const body = {
            body: {
                type: 'doc',
                version: 1,
                content: [
                    {
                        type: 'paragraph',
                        content: [
                            {
                                type: 'text',
                                text: comment
                            }
                        ]
                    }
                ]
            }
        };
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(body)
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Failed to add comment to JIRA issue ${issueKey}: ${response.status} ${error}`);
        }
    }
    /**
     * Add remote link to JIRA issue (for PR linking)
     *
     * POST /rest/api/3/issue/{issueIdOrKey}/remotelink
     * Uses globalId for idempotency — re-running won't create duplicates.
     */
    async addRemoteLink(issueKey, link) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/issue/${issueKey}/remotelink`;
        const body = {
            globalId: link.globalId,
            application: {
                type: 'com.specweave',
                name: 'SpecWeave'
            },
            relationship: link.relationship || 'pull request',
            object: {
                url: link.url,
                title: link.title,
                icon: {
                    url16x16: 'https://github.githubassets.com/favicons/favicon.svg',
                    title: 'GitHub'
                }
            }
        };
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(body)
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Failed to add remote link to JIRA issue ${issueKey}: ${response.status} ${error}`);
        }
    }
    /**
     * Execute raw JQL search query
     *
     * Used by FilterProcessor for custom JQL filtering
     *
     * @param jql - JQL query string
     * @param maxResults - Maximum results (default: 100)
     * @returns Search response with issues
     */
    async searchWithJql(jql, maxResults = 100) {
        const url = `${this.baseUrl}/rest/api/${this.apiVersion}/search/jql`;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': this.getAuthHeader(),
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                jql,
                maxResults,
                fields: ['project', 'summary'] // Minimal fields for filter processing
            })
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`JQL Search API Error (${response.status}): ${error}`);
        }
        const data = await response.json();
        return {
            issues: data.issues || [],
            total: data.total || 0
        };
    }
}
/**
 * Convert plain text (with optional markdown bold/italic) to Atlassian Document Format.
 * Splits on blank lines into paragraphs; converts **bold** and *italic* to ADF marks;
 * preserves --- as horizontal rules and lines starting with - as bullet lists.
 */
function textToAdf(text) {
    const lines = text.split('\n');
    const content = [];
    let i = 0;
    while (i < lines.length) {
        const line = lines[i];
        // Blank line — skip (paragraph break)
        if (line.trim() === '') {
            i++;
            continue;
        }
        // Horizontal rule
        if (line.trim() === '---') {
            content.push({ type: 'rule' });
            i++;
            continue;
        }
        // Bullet list: collect consecutive lines starting with -
        if (/^\s*-\s/.test(line)) {
            const items = [];
            while (i < lines.length && /^\s*-\s/.test(lines[i])) {
                const itemText = lines[i].replace(/^\s*-\s*/, '');
                items.push({
                    type: 'listItem',
                    content: [{ type: 'paragraph', content: inlineToAdf(itemText) }],
                });
                i++;
            }
            content.push({ type: 'bulletList', content: items });
            continue;
        }
        // Regular paragraph: collect lines until blank line, rule, or list item
        const paraLines = [];
        while (i < lines.length && lines[i].trim() !== '' && lines[i].trim() !== '---' && !/^\s*-\s/.test(lines[i])) {
            paraLines.push(lines[i]);
            i++;
        }
        const paraText = paraLines.join('\n');
        content.push({ type: 'paragraph', content: inlineToAdf(paraText) });
    }
    return { type: 'doc', version: 1, content: content.length > 0 ? content : [{ type: 'paragraph', content: [] }] };
}
/**
 * Convert inline markdown (bold, italic) to ADF inline nodes.
 */
function inlineToAdf(text) {
    const nodes = [];
    // Match **bold**, *italic*, or plain text segments
    const regex = /(\*\*(.+?)\*\*|\*(.+?)\*)/g;
    let lastIndex = 0;
    let match;
    while ((match = regex.exec(text)) !== null) {
        // Plain text before this match
        if (match.index > lastIndex) {
            const plain = text.substring(lastIndex, match.index);
            if (plain)
                nodes.push({ type: 'text', text: plain });
        }
        if (match[2]) {
            // **bold**
            nodes.push({ type: 'text', text: match[2], marks: [{ type: 'strong' }] });
        }
        else if (match[3]) {
            // *italic*
            nodes.push({ type: 'text', text: match[3], marks: [{ type: 'em' }] });
        }
        lastIndex = match.index + match[0].length;
    }
    // Remaining plain text
    if (lastIndex < text.length) {
        const remaining = text.substring(lastIndex);
        if (remaining)
            nodes.push({ type: 'text', text: remaining });
    }
    return nodes.length > 0 ? nodes : [{ type: 'text', text: text || ' ' }];
}
//# sourceMappingURL=jira-client.js.map