/**
 * Azure DevOps Importer
 *
 * Imports Azure DevOps work items as External Items with WIQL support and pagination.
 * Handles PAT authentication and converts ADO-specific data to platform-agnostic format.
 */
import { sanitizeHtmlForMdx } from '../utils/html-to-mdx.js';
/**
 * Azure DevOps Importer Implementation
 */
export class ADOImporter {
    constructor(orgUrl, project, pat) {
        this.platform = 'ado';
        this.orgUrl = orgUrl.replace(/\/+$/, ''); // Remove trailing slashes
        this.project = project;
        this.pat = pat || process.env.AZURE_DEVOPS_PAT || '';
        if (!this.pat) {
            throw new Error('Azure DevOps authentication required: Set AZURE_DEVOPS_PAT in .env file');
        }
    }
    /**
     * Import all work items matching config
     */
    async import(config = {}) {
        const items = [];
        for await (const page of this.paginate(config)) {
            items.push(...page);
        }
        return items;
    }
    /**
     * Paginate through work items using WIQL with date-based pagination for 100K+ items
     *
     * CRITICAL FIX (2025-12-01): Also fetch missing parent items (Epics/Capabilities)
     * Problem: If a parent Epic wasn't modified recently, it won't be in the WIQL results,
     * causing all its child User Stories to be grouped into one generic folder.
     * Solution: After fetching all items, identify missing parents and fetch them separately.
     *
     * ENHANCEMENT (2025-12-01): Date-based pagination to handle 100K+ items
     * ADO WIQL has a 20K ID limit per query. For larger projects, we need to:
     * 1. Run initial WIQL query (up to 20K IDs)
     * 2. If we hit the limit, run another query with older date range
     * 3. Keep paginating by date until we have all items or hit maxItems
     */
    async *paginate(config = {}) {
        const { timeRangeMonths = 3, includeClosed = false, labels = [], maxItems = Infinity, } = config;
        // Collect all items to find missing parents after pagination
        const allFetchedItems = [];
        const fetchedIds = new Set();
        const top = 200; // ADO pagination size (max 200)
        let totalFetched = 0;
        // WIQL has a 20K limit per query - use date-based pagination for larger datasets
        const WIQL_LIMIT = 20000;
        let currentEndDate = new Date(); // Start from now
        const sinceDate = new Date();
        sinceDate.setMonth(sinceDate.getMonth() - timeRangeMonths);
        try {
            // Date-based pagination loop
            let dateRangeIteration = 0;
            let hasMoreItems = true;
            while (hasMoreItems && totalFetched < maxItems && currentEndDate > sinceDate) {
                dateRangeIteration++;
                // Build WIQL query for this date range
                const sinceDateStr = sinceDate.toISOString().split('T')[0];
                const untilDateStr = currentEndDate.toISOString().split('T')[0];
                const wiqlParts = [
                    `SELECT [System.Id], [System.Title], [System.WorkItemType], [System.State], [System.ChangedDate]`,
                    `FROM WorkItems`,
                    `WHERE [System.TeamProject] = '${this.project}'`,
                    // Use ChangedDate range for pagination
                    `AND [System.ChangedDate] >= '${sinceDateStr}'`,
                ];
                // For subsequent iterations, add upper bound to avoid duplicates
                if (dateRangeIteration > 1) {
                    wiqlParts.push(`AND [System.ChangedDate] < '${untilDateStr}'`);
                }
                wiqlParts.push(
                // Filter for relevant work item types (includes Capability for enterprise setups)
                `AND [System.WorkItemType] IN ('Capability', 'Epic', 'Feature', 'User Story', 'Product Backlog Item', 'Bug', 'Task')`);
                // Status filter
                if (!includeClosed) {
                    wiqlParts.push(`AND [System.State] <> 'Closed' AND [System.State] <> 'Removed'`);
                }
                // Tags filter (ADO uses semicolon-separated tags)
                if (labels.length > 0) {
                    const tagsCondition = labels.map((tag) => `[System.Tags] CONTAINS '${tag}'`).join(' OR ');
                    wiqlParts.push(`AND (${tagsCondition})`);
                }
                wiqlParts.push(`ORDER BY [System.ChangedDate] DESC`);
                const wiql = wiqlParts.join(' ');
                // Execute WIQL query - use high limit to maximize items per query
                const queryResult = await this.makeADORequest(`/_apis/wit/wiql?$top=20000&api-version=7.0`, {
                    method: 'POST',
                    body: JSON.stringify({ query: wiql }),
                });
                const workItemIds = queryResult.workItems.map((wi) => wi.id);
                // Filter out already fetched IDs (avoid duplicates across date ranges)
                const newIds = workItemIds.filter(id => !fetchedIds.has(id));
                console.log(`   📊 WIQL iteration ${dateRangeIteration}: ${workItemIds.length} IDs found (${newIds.length} new)`);
                if (newIds.length === 0) {
                    // No new items - we're done or need to go further back in time
                    if (workItemIds.length >= WIQL_LIMIT) {
                        // Hit the limit but all were duplicates - shouldn't happen, but handle it
                        hasMoreItems = false;
                    }
                    else {
                        hasMoreItems = false;
                    }
                    continue;
                }
                // Paginate through work item IDs
                let skip = 0;
                let oldestChangedDate = null;
                while (skip < newIds.length && totalFetched < maxItems) {
                    const ids = newIds.slice(skip, skip + top);
                    if (ids.length === 0) {
                        break;
                    }
                    // Fetch full work item details
                    const workItems = await this.getWorkItemsBatch(ids);
                    // Track fetched IDs and find oldest date for next iteration
                    for (const wi of workItems) {
                        fetchedIds.add(wi.fields['System.Id']);
                        const changedDate = new Date(wi.fields['System.ChangedDate']);
                        if (!oldestChangedDate || changedDate < oldestChangedDate) {
                            oldestChangedDate = changedDate;
                        }
                    }
                    // Convert to ExternalItems
                    const items = workItems.map((wi) => this.convertToExternalItem(wi));
                    allFetchedItems.push(...items);
                    // Yield page
                    if (items.length > 0) {
                        const itemsToYield = items.slice(0, maxItems - totalFetched);
                        yield itemsToYield;
                        totalFetched += itemsToYield.length;
                    }
                    skip += top;
                }
                // Check if we hit the WIQL limit and need another date range iteration
                if (workItemIds.length >= WIQL_LIMIT && oldestChangedDate && totalFetched < maxItems) {
                    // Move date window back to continue fetching older items
                    currentEndDate = oldestChangedDate;
                    console.log(`   📅 Hit WIQL limit, continuing with items before ${currentEndDate.toISOString().split('T')[0]}...`);
                }
                else {
                    hasMoreItems = false;
                }
            }
            console.log(`   ✅ Total items fetched: ${totalFetched}`);
            // Step 3: CRITICAL - Fetch missing parent items (Epics/Capabilities not in time range)
            // This ensures proper hierarchy grouping even for old parent items
            const missingParentIds = this.findMissingParentIds(allFetchedItems, fetchedIds);
            if (missingParentIds.length > 0) {
                console.log(`   📥 Fetching ${missingParentIds.length} parent items (Epics/Capabilities) for hierarchy...`);
                // Fetch missing parents in batches
                for (let i = 0; i < missingParentIds.length; i += top) {
                    const batchIds = missingParentIds.slice(i, i + top);
                    const parentWorkItems = await this.getWorkItemsBatch(batchIds);
                    const parentItems = parentWorkItems.map((wi) => this.convertToExternalItem(wi));
                    // Yield parent items (they need to be processed for grouping)
                    if (parentItems.length > 0) {
                        yield parentItems;
                    }
                }
            }
        }
        catch (error) {
            if (error.status === 401) {
                throw new Error(`Azure DevOps authentication failed: ${error.message}`);
            }
            else if (error.status === 403) {
                throw new Error(`Azure DevOps access forbidden: ${error.message}`);
            }
            throw error;
        }
    }
    /**
     * Find parent IDs that are referenced but not in our fetched items
     * These are typically Epics/Capabilities that weren't modified recently
     */
    findMissingParentIds(items, fetchedIds) {
        const missingIds = new Set();
        for (const item of items) {
            if (item.parentId) {
                // Extract numeric ID from "ADO-123" format
                const numericId = parseInt(item.parentId.replace('ADO-', ''), 10);
                if (!isNaN(numericId) && !fetchedIds.has(numericId)) {
                    missingIds.add(numericId);
                }
            }
        }
        return Array.from(missingIds);
    }
    /**
     * Fetch work items in batch (up to 200 IDs)
     */
    async getWorkItemsBatch(ids) {
        if (ids.length === 0) {
            return [];
        }
        const idsParam = ids.join(',');
        const fields = [
            'System.Id',
            'System.Title',
            'System.Description',
            'System.WorkItemType',
            'System.State',
            'System.CreatedDate',
            'System.ChangedDate',
            'System.Tags',
            'System.AreaPath', // CRITICAL: Required for organizing items by area path
            'Microsoft.VSTS.Common.Priority',
            'Microsoft.VSTS.Common.AcceptanceCriteria',
            'System.Parent',
        ].join(',');
        const response = await this.makeADORequest(`/_apis/wit/workitems?ids=${idsParam}&fields=${fields}&$expand=Links&api-version=7.0`);
        return response.value;
    }
    /**
     * Make authenticated ADO API request
     */
    async makeADORequest(endpoint, options = {}) {
        const url = `${this.orgUrl}/${this.project}${endpoint}`;
        // Create Basic Auth header (PAT as username, empty password)
        const auth = Buffer.from(`:${this.pat}`).toString('base64');
        const response = await fetch(url, {
            method: options.method || 'GET',
            headers: {
                'Authorization': `Basic ${auth}`,
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                ...options.headers,
            },
            body: options.body,
        });
        if (!response.ok) {
            const errorText = await response.text();
            const error = new Error(`ADO API error: ${response.statusText} - ${errorText}`);
            error.status = response.status;
            throw error;
        }
        return response.json();
    }
    /**
     * Convert ADO work item to ExternalItem
     */
    convertToExternalItem(workItem) {
        const fields = workItem.fields;
        const witType = fields['System.WorkItemType'].toLowerCase();
        const state = fields['System.State'].toLowerCase();
        return {
            id: `ADO-${fields['System.Id']}`,
            type: this.mapWorkItemType(witType),
            title: fields['System.Title'],
            description: sanitizeHtmlForMdx(fields['System.Description']),
            status: this.mapState(state),
            priority: this.mapPriority(fields['Microsoft.VSTS.Common.Priority']),
            createdAt: new Date(fields['System.CreatedDate']),
            updatedAt: new Date(fields['System.ChangedDate']),
            url: workItem._links.html.href,
            labels: fields['System.Tags']
                ? fields['System.Tags'].split(';').map((t) => t.trim()).filter(Boolean)
                : [],
            acceptanceCriteria: fields['Microsoft.VSTS.Common.AcceptanceCriteria']
                ? this.parseAcceptanceCriteria(fields['Microsoft.VSTS.Common.AcceptanceCriteria'])
                : undefined,
            parentId: fields['System.Parent'] ? `ADO-${fields['System.Parent']}` : undefined,
            platform: 'ado',
            adoProjectName: this.project,
            adoAreaPath: fields['System.AreaPath'],
            adoWorkItemType: fields['System.WorkItemType'],
        };
    }
    /**
     * Map ADO work item type to ExternalItem type
     */
    mapWorkItemType(witType) {
        const typeMap = {
            'user story': 'user-story',
            'product backlog item': 'user-story',
            'epic': 'epic',
            'capability': 'epic', // Capability is above Epic in enterprise ADO
            'bug': 'bug',
            'feature': 'feature',
        };
        return typeMap[witType] || 'task';
    }
    /**
     * Map ADO state to ExternalItem status
     */
    mapState(state) {
        const inProgressStates = ['active', 'in progress', 'committed'];
        const completedStates = ['closed', 'done', 'resolved'];
        if (inProgressStates.includes(state))
            return 'in-progress';
        if (completedStates.includes(state))
            return 'completed';
        return 'open';
    }
    /**
     * Map ADO priority (1-4) to ExternalItem priority (P0-P4)
     */
    mapPriority(priorityValue) {
        if (priorityValue === undefined)
            return undefined;
        const priorityMap = {
            1: 'P0',
            2: 'P1',
            3: 'P2',
            4: 'P3',
        };
        return priorityMap[priorityValue] || 'P4';
    }
    /**
     * Parse acceptance criteria from ADO HTML format
     * ADO stores ACs as HTML, needs to extract plain text
     */
    parseAcceptanceCriteria(html) {
        // Strip HTML tags and extract bullet points
        const text = html
            .replace(/<[^>]*>/g, '') // Remove HTML tags
            .replace(/&nbsp;/g, ' ')
            .replace(/&amp;/g, '&')
            .replace(/&lt;/g, '<')
            .replace(/&gt;/g, '>')
            .replace(/&quot;/g, '"');
        // Split by newlines and filter empty lines
        const lines = text
            .split('\n')
            .map((line) => line.trim())
            .filter(Boolean);
        return lines.length > 0 ? lines : undefined;
    }
}
//# sourceMappingURL=ado-importer.js.map