"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JiraProvider = void 0;
const logger_1 = require("../logger");
const assets_1 = require("./assets");
class JiraProvider {
    constructor(config) {
        this.baseUrl = config.jiraBaseUrl.replace(/\/$/, '');
        this.authHeader =
            'Basic ' + Buffer.from(`${config.jiraEmail}:${config.jiraApiToken}`).toString('base64');
        this.project = config.jiraProject;
        this.label = config.jiraLabel;
        this.assigneeTag = config.assigneeTag;
        this.inReviewStatus = config.jiraInReviewStatus || 'In Review';
    }
    async request(path, options = {}) {
        const url = `${this.baseUrl}/rest/api/3${path}`;
        const res = await fetch(url, {
            ...options,
            headers: {
                Authorization: this.authHeader,
                'Content-Type': 'application/json',
                Accept: 'application/json',
                ...(options.headers || {}),
            },
        });
        if (!res.ok) {
            const body = await res.text();
            throw new Error(`Jira API error ${res.status}: ${body}`);
        }
        return res.json();
    }
    buildAuthHeaders() {
        return {
            Authorization: this.authHeader,
            Accept: '*/*',
        };
    }
    /** Extract plain text from an Atlassian Document Format node. */
    adfToText(node) {
        return this.normalizeAdfText(this.adfToTextNode(node));
    }
    adfToTextNode(node) {
        if (!node || typeof node !== 'object')
            return '';
        const n = node;
        if (n.type === 'text' && typeof n.text === 'string')
            return n.text;
        if (n.type === 'hardBreak')
            return '\n';
        if (Array.isArray(n.content)) {
            const children = n.content.map((c) => this.adfToTextNode(c)).join('');
            if (n.type === 'paragraph' || n.type === 'heading' || n.type === 'listItem') {
                return `${children}\n`;
            }
            return children;
        }
        return '';
    }
    normalizeAdfText(text) {
        return text.replace(/\n{3,}/g, '\n\n').trim();
    }
    async downloadIssueAttachments(taskId, attachments) {
        const nativeAttachments = attachments
            .filter((attachment) => Boolean(attachment.content))
            .map((attachment) => ({
            id: attachment.id !== undefined ? String(attachment.id) : undefined,
            name: attachment.filename,
            url: attachment.content,
        }));
        return (0, assets_1.downloadAttachments)(taskId, nativeAttachments, {
            headers: this.buildAuthHeaders(),
        });
    }
    async fetchIssueAttachments(taskId) {
        const issue = await this.request(`/issue/${taskId}?fields=attachment`);
        return this.downloadIssueAttachments(taskId, issue.fields.attachment || []);
    }
    collectAttachmentReferences(node, refs = []) {
        if (!node || typeof node !== 'object')
            return refs;
        const record = node;
        const attrs = this.asRecord(record.attrs);
        if (record.type === 'media') {
            const id = typeof attrs?.id === 'string' ? attrs.id : undefined;
            const url = typeof attrs?.url === 'string' ? attrs.url : undefined;
            if (id || url)
                refs.push({ id, url });
        }
        if (Array.isArray(record.marks)) {
            for (const mark of record.marks) {
                const markRecord = this.asRecord(mark);
                const markAttrs = this.asRecord(markRecord?.attrs);
                if (markRecord?.type === 'link' && typeof markAttrs?.href === 'string') {
                    refs.push({ url: markAttrs.href });
                }
            }
        }
        if (Array.isArray(record.content)) {
            for (const child of record.content) {
                this.collectAttachmentReferences(child, refs);
            }
        }
        return refs;
    }
    resolveCommentAttachments(body, attachments) {
        const refs = this.collectAttachmentReferences(body);
        const matched = [];
        const seen = new Set();
        for (const ref of refs) {
            const attachment = this.findAttachmentForReference(ref, attachments);
            if (!attachment || seen.has(attachment.path))
                continue;
            seen.add(attachment.path);
            matched.push(attachment);
        }
        return matched;
    }
    findAttachmentForReference(ref, attachments) {
        if (ref.id) {
            const byId = attachments.find((attachment) => attachment.id === ref.id);
            if (byId)
                return byId;
        }
        if (!ref.url)
            return undefined;
        const absoluteUrl = this.toAbsoluteUrl(ref.url);
        const attachmentId = this.extractAttachmentId(ref.url);
        if (attachmentId) {
            const byId = attachments.find((attachment) => attachment.id === attachmentId);
            if (byId)
                return byId;
        }
        const normalized = (0, assets_1.normalizeAttachmentUrl)(absoluteUrl);
        return attachments.find((attachment) => (0, assets_1.normalizeAttachmentUrl)(attachment.sourceUrl) === normalized);
    }
    toAbsoluteUrl(url) {
        try {
            return new URL(url, this.baseUrl).toString();
        }
        catch {
            return url;
        }
    }
    extractAttachmentId(url) {
        try {
            const pathname = new URL(url, this.baseUrl).pathname;
            const match = pathname.match(/\/attachment\/content\/(\d+)/)
                || pathname.match(/\/attachment\/(\d+)/)
                || pathname.match(/\/secure\/attachment\/(\d+)/);
            return match?.[1] || null;
        }
        catch {
            return null;
        }
    }
    asRecord(value) {
        return value && typeof value === 'object' ? value : null;
    }
    async fetchTasks(options) {
        logger_1.logger.debug(`Fetching Jira issues in project "${this.project}" with label "${this.label}"`);
        const skipAttachments = options?.skipAttachments === true;
        const omitDescription = options?.omitDescription === true;
        const labelClause = this.label === '*' ? '' : ` AND labels = "${this.label}"`;
        const jql = `project = "${this.project}"${labelClause} AND statusCategory != Done ORDER BY created DESC`;
        const fields = 'summary,description,status,priority,labels,attachment,project';
        const data = await this.request(`/search/jql?jql=${encodeURIComponent(jql)}&fields=${fields}&maxResults=50`);
        return Promise.all(data.issues.map(async (issue) => {
            let description = omitDescription ? '' : this.adfToText(issue.fields.description);
            if (!skipAttachments && !omitDescription) {
                try {
                    const attachments = await this.downloadIssueAttachments(issue.key, issue.fields.attachment || []);
                    description = (0, assets_1.appendAttachmentPaths)(description, attachments);
                }
                catch (err) {
                    logger_1.logger.warn(`[${issue.key}] Failed to download Jira attachments: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
            return {
                id: issue.key,
                name: issue.fields.summary,
                description,
                status: issue.fields.status.name.toLowerCase(),
                url: `${this.baseUrl}/browse/${issue.key}`,
                tags: issue.fields.labels,
                priority: issue.fields.priority ? parseInt(issue.fields.priority.id, 10) : undefined,
                sourceListId: issue.fields.project?.key,
            };
        }));
    }
    async fetchTasksByStatus(statuses, options) {
        const normalized = statuses.map((s) => s.toLowerCase());
        const all = await this.fetchTasks(options);
        return all.filter((t) => normalized.includes(t.status.toLowerCase()));
    }
    async postComment(taskId, text) {
        logger_1.logger.debug(`Posting comment to Jira issue ${taskId}`);
        await this.request(`/issue/${taskId}/comment`, {
            method: 'POST',
            body: JSON.stringify({
                body: {
                    type: 'doc',
                    version: 1,
                    content: [
                        {
                            type: 'paragraph',
                            content: [{ type: 'text', text }],
                        },
                    ],
                },
            }),
        });
    }
    async getComments(taskId) {
        logger_1.logger.debug(`Fetching comments for Jira issue ${taskId}`);
        let issueAttachments = [];
        try {
            issueAttachments = await this.fetchIssueAttachments(taskId);
        }
        catch (err) {
            logger_1.logger.warn(`[${taskId}] Failed to fetch Jira attachments for comment context: ${err instanceof Error ? err.message : String(err)}`);
        }
        const data = await this.request(`/issue/${taskId}/comment`);
        // Sort ascending by date so newest is always last (consistent with ClickUp provider)
        const sorted = [...data.comments].sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
        return sorted.map((c) => ({
            id: c.id,
            text: (0, assets_1.appendAttachmentPaths)(this.adfToText(c.body), this.resolveCommentAttachments(c.body, issueAttachments), 'Local asset files referenced by this comment (read/use these if relevant):'),
            author: c.author.displayName,
            authorId: c.author.accountId,
            date: new Date(c.created).getTime(),
        }));
    }
    async fetchAvailableStatuses() {
        const data = await this.request(`/project/${encodeURIComponent(this.project)}/statuses`);
        const seen = new Set();
        for (const issueType of data) {
            for (const s of issueType.statuses || []) {
                if (s.name)
                    seen.add(s.name);
            }
        }
        return [...seen];
    }
    async updateStatus(taskId, status) {
        logger_1.logger.debug(`Transitioning Jira issue ${taskId} to "${status}"`);
        const data = await this.request(`/issue/${taskId}/transitions`);
        const transition = data.transitions.find((t) => t.name.toLowerCase() === status.toLowerCase());
        if (!transition) {
            const names = data.transitions.map((t) => t.name).join(', ');
            throw new Error(`Jira transition "${status}" not found for issue ${taskId}. Available: ${names}`);
        }
        await this.request(`/issue/${taskId}/transitions`, {
            method: 'POST',
            body: JSON.stringify({ transition: { id: transition.id } }),
        });
    }
    async fetchBoardTasks(options) {
        const boardOpts = { skipAttachments: true, omitDescription: true, ...options };
        const labeled = await this.fetchTasks(boardOpts);
        const labelClauseReview = this.label === '*' ? '' : ` AND labels = "${this.label}"`;
        const reviewJql = `project = "${this.project}"${labelClauseReview} AND status = "${this.inReviewStatus}" AND statusCategory != Done ORDER BY created DESC`;
        let reviewTasks = [];
        try {
            const data = await this.request(`/search/jql?jql=${encodeURIComponent(reviewJql)}&fields=summary,status,priority,labels,project&maxResults=50`);
            reviewTasks = data.issues.map((issue) => ({
                id: issue.key,
                name: issue.fields.summary,
                description: '',
                status: issue.fields.status.name.toLowerCase(),
                url: `${this.baseUrl}/browse/${issue.key}`,
                tags: issue.fields.labels,
                priority: issue.fields.priority ? parseInt(issue.fields.priority.id, 10) : undefined,
                sourceListId: issue.fields.project?.key,
            }));
        }
        catch (err) {
            logger_1.logger.warn(`Failed to fetch Jira review tasks: ${err instanceof Error ? err.message : err}`);
        }
        const byId = new Map();
        for (const t of labeled)
            byId.set(t.id, t);
        for (const t of reviewTasks)
            byId.set(t.id, t);
        return [...byId.values()];
    }
    async removeTag(taskId, tag) {
        logger_1.logger.debug(`Removing label "${tag}" from Jira issue ${taskId}`);
        await this.request(`/issue/${taskId}`, {
            method: 'PUT',
            body: JSON.stringify({
                update: { labels: [{ remove: tag }] },
            }),
        });
    }
    async addTag(taskId, tag) {
        logger_1.logger.debug(`Adding label "${tag}" to Jira issue ${taskId}`);
        await this.request(`/issue/${taskId}`, {
            method: 'PUT',
            body: JSON.stringify({
                update: { labels: [{ add: tag }] },
            }),
        });
    }
    async createTask(params) {
        const project = params.listId || this.project;
        const fields = {
            project: { key: project },
            summary: params.title,
            description: {
                type: 'doc',
                version: 1,
                content: [
                    { type: 'paragraph', content: [{ type: 'text', text: params.description || ' ' }] },
                ],
            },
            issuetype: { name: 'Task' },
            labels: params.tags,
        };
        if (params.priority) {
            const names = { 1: 'Highest', 2: 'High', 3: 'Medium', 4: 'Low' };
            fields.priority = { name: names[params.priority] || 'Medium' };
        }
        if (params.dueDate) {
            fields.duedate = new Date(params.dueDate).toISOString().split('T')[0];
        }
        const result = await this.request('/issue', {
            method: 'POST',
            body: JSON.stringify({ fields }),
        });
        return { id: result.key, url: `${this.baseUrl}/browse/${result.key}` };
    }
}
exports.JiraProvider = JiraProvider;
//# sourceMappingURL=jira.js.map