"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClickUpProvider = void 0;
const logger_1 = require("../logger");
const assets_1 = require("./assets");
const clickup_format_1 = require("./clickup-format");
function mergeById(primary, extra) {
    const byId = new Map();
    for (const t of primary)
        byId.set(t.id, t);
    for (const t of extra)
        byId.set(t.id, t);
    return [...byId.values()];
}
class ClickUpProvider {
    constructor(config) {
        this.apiKey = config.clickupApiKey;
        this.teamId = config.clickupTeamId;
        this.tag = config.clickupTag;
        this.assigneeTag = config.assigneeTag;
        this.listId = config.clickupListId;
        this.pendingStatus = config.clickupPendingStatus || 'pending';
        this.openStatus = config.clickupOpenStatus || 'open';
        this.inReviewStatus = config.clickupInReviewStatus || 'review';
    }
    async request(path, options = {}) {
        const url = `https://api.clickup.com/api/v2${path}`;
        const maxAttempts = 3;
        let lastError;
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                const res = await fetch(url, {
                    ...options,
                    headers: {
                        Authorization: this.apiKey,
                        'Content-Type': 'application/json',
                        ...(options.headers || {}),
                    },
                });
                if (!res.ok) {
                    const body = await res.text();
                    throw new Error(`ClickUp API error ${res.status}: ${body}`);
                }
                return res.json();
            }
            catch (err) {
                lastError = err instanceof Error ? err : new Error(String(err));
                const isNetworkError = lastError.message.includes('fetch failed')
                    || lastError.message.includes('ECONNRESET')
                    || lastError.message.includes('ETIMEDOUT')
                    || lastError.message.includes('UND_ERR_SOCKET');
                if (!isNetworkError || attempt === maxAttempts) {
                    const cause = err?.cause;
                    const detail = cause instanceof Error ? `: ${cause.message}` : '';
                    throw new Error(`ClickUp API request failed (${options.method || 'GET'} ${path})${detail}`);
                }
                const delay = attempt * 1000;
                logger_1.logger.debug(`Fetch attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms...`);
                await new Promise((resolve) => setTimeout(resolve, delay));
            }
        }
        throw lastError;
    }
    buildAuthHeaders() {
        return {
            Authorization: this.apiKey,
            Accept: '*/*',
        };
    }
    async fetchTaggedTeamTasks() {
        const tagFilter = this.tag === '*' ? '' : `tags[]=${encodeURIComponent(this.tag)}&`;
        const data = await this.request(`/team/${this.teamId}/task?${tagFilter}subtasks=true&include_closed=false&include_markdown_description=true`);
        return data.tasks;
    }
    async mapRawTasks(tasks, options) {
        const skipAttachments = options?.skipAttachments === true;
        const omitDescription = options?.omitDescription === true;
        if (skipAttachments && omitDescription) {
            return tasks.map((t) => this.mapRawTaskSync(t, '', options));
        }
        return Promise.all(tasks.map(async (t) => {
            let description = omitDescription ? '' : (t.markdown_description || t.description || '');
            if (!skipAttachments) {
                try {
                    const attachments = await this.fetchTaskAttachments(t.id);
                    description = (0, assets_1.appendAttachmentPaths)(description, attachments);
                }
                catch (err) {
                    logger_1.logger.warn(`[${t.id}] Failed to fetch ClickUp attachments: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
            return this.mapRawTaskSync(t, description, options);
        }));
    }
    mapRawTaskSync(t, description, _options) {
        return {
            id: t.id,
            name: t.name,
            description,
            status: t.status.status.toLowerCase(),
            url: t.url,
            tags: t.tags.map((tag) => tag.name),
            priority: t.priority ? parseInt(t.priority.id, 10) : undefined,
            sourceListId: t.list?.id,
        };
    }
    async fetchTaskAttachments(taskId) {
        const detail = await this.request(`/task/${taskId}`);
        const attachments = (detail.attachments || [])
            .filter((attachment) => Boolean(attachment.url))
            .map((attachment) => ({
            id: attachment.id !== undefined ? String(attachment.id) : undefined,
            name: attachment.title,
            url: attachment.url,
        }));
        return (0, assets_1.downloadAttachments)(taskId, attachments, {
            headers: this.buildAuthHeaders(),
        });
    }
    async fetchTeamTasksByStatus(statuses) {
        const normalized = statuses.map((s) => s.toLowerCase());
        const data = await this.request(`/team/${this.teamId}/task?subtasks=true&include_closed=false&include_markdown_description=true`);
        return data.tasks.filter((t) => normalized.includes(t.status.status.toLowerCase()));
    }
    async fetchBoardTasks(options) {
        logger_1.logger.debug(`Fetching board tasks with tag "${this.tag}" from team ${this.teamId}`);
        const boardOpts = {
            skipAttachments: true,
            omitDescription: true,
            ...options,
        };
        const pendingStatus = this.pendingStatus.toLowerCase();
        const openStatus = this.openStatus.toLowerCase();
        const inProgress = ClickUpProvider.IN_PROGRESS_STATUS;
        const inReviewStatus = this.inReviewStatus.toLowerCase();
        // Tagged tasks for open/pending/in-progress columns
        const tagged = await this.fetchTaggedTeamTasks();
        const eligible = tagged.filter((t) => {
            const status = t.status.status.toLowerCase();
            return status === openStatus || status === pendingStatus || status === inProgress;
        });
        const reviewRawAll = await this.fetchTeamTasksByStatus([inReviewStatus]);
        const reviewRaw = this.tag === '*'
            ? reviewRawAll
            : reviewRawAll.filter((t) => t.tags.some((tag) => tag.name.toLowerCase() === this.tag.toLowerCase()));
        const [taggedMapped, reviewMapped] = await Promise.all([
            this.mapRawTasks(eligible, boardOpts),
            this.mapRawTasks(reviewRaw, boardOpts),
        ]);
        return mergeById(taggedMapped, reviewMapped);
    }
    async fetchTaskById(taskId, options) {
        try {
            const t = await this.request(`/task/${taskId}?include_markdown_description=true`);
            const mapped = await this.mapRawTasks([t], options);
            return mapped[0] ?? null;
        }
        catch {
            return null;
        }
    }
    async fetchTasks(options) {
        logger_1.logger.debug(`Fetching tasks with tag "${this.tag}" from team ${this.teamId}`);
        const pendingStatus = this.pendingStatus.toLowerCase();
        const openStatus = this.openStatus.toLowerCase();
        const all = await this.fetchTaggedTeamTasks();
        const eligibleTasks = all.filter((t) => {
            const status = t.status.status.toLowerCase();
            return status === openStatus || status === pendingStatus;
        });
        return this.mapRawTasks(eligibleTasks, options);
    }
    async fetchTasksByStatus(statuses, options) {
        const normalized = statuses.map((s) => s.toLowerCase());
        const all = await this.fetchTaggedTeamTasks();
        const eligibleTasks = all.filter((t) => normalized.includes(t.status.status.toLowerCase()));
        return this.mapRawTasks(eligibleTasks, options);
    }
    async postComment(taskId, text) {
        logger_1.logger.debug(`Posting comment to task ${taskId}`);
        const blocks = (0, clickup_format_1.markdownToClickupBlocks)(text);
        await this.request(`/task/${taskId}/comment`, {
            method: 'POST',
            body: JSON.stringify({ comment: blocks }),
        });
    }
    async getComments(taskId) {
        logger_1.logger.debug(`Fetching comments for task ${taskId}`);
        const data = await this.request(`/task/${taskId}/comment`);
        // ClickUp returns comments newest-first; sort ascending so newest is last
        const sorted = [...data.comments].sort((a, b) => parseInt(a.date, 10) - parseInt(b.date, 10));
        return sorted.map((c) => {
            let text;
            if (typeof c.comment_text === 'string' && c.comment_text) {
                text = c.comment_text;
            }
            else if (Array.isArray(c.comment_text) && c.comment_text.length > 0) {
                text = (0, clickup_format_1.clickupBlocksToMarkdown)(c.comment_text);
            }
            else if (Array.isArray(c.comment) && c.comment.length > 0) {
                text = (0, clickup_format_1.clickupBlocksToMarkdown)(c.comment);
            }
            else {
                text = '';
            }
            return {
                id: c.id,
                text,
                author: c.user.username,
                authorId: String(c.user.id),
                date: parseInt(c.date, 10),
            };
        });
    }
    async fetchAvailableStatuses() {
        const seen = new Set();
        if (this.listId) {
            try {
                const list = await this.request(`/list/${this.listId}`);
                for (const s of list.statuses || []) {
                    if (s.status)
                        seen.add(s.status);
                }
            }
            catch {
                // fall through to task-based discovery
            }
        }
        if (seen.size === 0) {
            const tagFilter = this.tag === '*' ? '' : `tags[]=${encodeURIComponent(this.tag)}&`;
            const data = await this.request(`/team/${this.teamId}/task?${tagFilter}subtasks=true&include_closed=true`);
            for (const t of data.tasks) {
                if (t.status?.status)
                    seen.add(t.status.status);
            }
        }
        return [...seen];
    }
    async updateStatus(taskId, status) {
        logger_1.logger.debug(`Updating task ${taskId} status to "${status}"`);
        await this.request(`/task/${taskId}`, {
            method: 'PUT',
            body: JSON.stringify({ status }),
        });
    }
    async removeTag(taskId, tag) {
        logger_1.logger.debug(`Removing tag "${tag}" from task ${taskId}`);
        await this.request(`/task/${taskId}/tag/${encodeURIComponent(tag)}`, {
            method: 'DELETE',
        });
    }
    async addTag(taskId, tag) {
        logger_1.logger.debug(`Adding tag "${tag}" to task ${taskId}`);
        await this.request(`/task/${taskId}/tag/${encodeURIComponent(tag)}`, {
            method: 'POST',
        });
    }
    async createTask(params) {
        const listId = params.listId || this.listId;
        if (!listId) {
            throw new Error('Cannot create task: no ClickUp list ID configured. Set CLICKUP_LIST_ID in .env.aidev or specify listId on the task.');
        }
        const body = {
            name: params.title,
            markdown_description: params.description,
            tags: params.tags,
        };
        if (params.priority)
            body.priority = params.priority;
        if (params.dueDate) {
            body.due_date = params.dueDate;
            body.due_date_time = true;
        }
        const result = await this.request(`/list/${listId}/task`, {
            method: 'POST',
            body: JSON.stringify(body),
        });
        return { id: result.id, url: result.url };
    }
}
exports.ClickUpProvider = ClickUpProvider;
ClickUpProvider.IN_PROGRESS_STATUS = 'in progress';
//# sourceMappingURL=clickup.js.map