/**
 * Azure DevOps Provider Adapter
 *
 * Implements ProviderAdapter for ADO REST API v7.1.
 */
import { generateLabels, } from '../engine.js';
import { formatIssueTitle } from '../types.js';
const ADO_CLOSED_STATES = ['Closed', 'Done', 'Resolved', 'Removed'];
const ADO_OPEN_STATES = ['Active', 'New', 'In Progress', 'Committed'];
const TEMPLATE_CLOSED_MAP = {
    Basic: 'Done',
    Agile: 'Closed',
    Scrum: 'Done',
    CMMI: 'Closed',
    SAFe: 'Closed',
};
const TEMPLATE_ACTIVE_MAP = {
    Basic: 'Doing',
    Agile: 'Active',
    Scrum: 'Committed',
    CMMI: 'Active',
    SAFe: 'Active',
};
const TEMPLATE_WIT_MAP = {
    Basic: 'Issue',
    Agile: 'User Story',
    Scrum: 'Product Backlog Item',
    CMMI: 'Requirement',
    SAFe: 'User Story',
};
// ---------------------------------------------------------------------------
// AdoAdapter
// ---------------------------------------------------------------------------
export class AdoAdapter {
    constructor(config) {
        this.platform = 'ado';
        this.suffix = 'A';
        this.templateDetected = false;
        this.organization = config.organization;
        this.project = config.project;
        this.pat = config.pat;
        this.baseUrl = `https://dev.azure.com/${this.organization}/${this.project}/_apis`;
        this.config = config;
    }
    async testConnection() {
        try {
            const response = await this.apiRequest('GET', `https://dev.azure.com/${this.organization}/_apis/projects/${this.project}?api-version=7.1`);
            if (response.ok)
                return { ok: true };
            const text = await response.text();
            return { ok: false, error: `HTTP ${response.status}: ${text}` };
        }
        catch (error) {
            return { ok: false, error: String(error) };
        }
    }
    async createIssue(story, feature) {
        const title = formatIssueTitle(story.id, story.title);
        const description = this.buildDescription(story, feature);
        const operations = [
            { op: 'add', path: '/fields/System.Title', value: title },
            { op: 'add', path: '/fields/System.Description', value: description },
            { op: 'add', path: '/fields/System.Tags', value: `specweave;priority:${story.priority || 'P2'}` },
        ];
        if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
            const acHtml = story.acceptanceCriteria.map(ac => `<li>${ac}</li>`).join('');
            operations.push({ op: 'add', path: '/fields/Microsoft.VSTS.Common.AcceptanceCriteria', value: `<ul>${acHtml}</ul>` });
        }
        // Parent linking: add hierarchy relation when feature has an ADO work item
        if (feature.externalRef?.id) {
            operations.push({
                op: 'add',
                path: '/relations/-',
                value: {
                    rel: 'System.LinkTypes.Hierarchy-Reverse',
                    url: `https://dev.azure.com/${this.organization}/${this.project}/_apis/wit/workItems/${feature.externalRef.id}`,
                },
            });
        }
        // Work item type: resolve from process template, fall back to config
        const template = await this.getTemplate();
        const wit = template
            ? (TEMPLATE_WIT_MAP[template.template] ?? this.config.workItemType ?? 'User Story')
            : (this.config.workItemType ?? 'User Story');
        const workItemType = encodeURIComponent(wit);
        const response = await this.apiRequest('POST', `/wit/workitems/$${workItemType}?api-version=7.1`, operations, 'application/json-patch+json');
        if (!response.ok) {
            const text = await response.text();
            throw new Error(`Failed to create work item: ${response.status} ${text.substring(0, 200)}`);
        }
        const workItem = await response.json();
        if (!workItem.id) {
            throw new Error(`Work item created but no ID returned. Response: ${JSON.stringify(workItem).substring(0, 200)}`);
        }
        const htmlUrl = workItem._links?.html?.href ||
            `https://dev.azure.com/${this.organization}/${this.project}/_workitems/edit/${workItem.id}`;
        return {
            platform: 'ado',
            id: String(workItem.id),
            url: htmlUrl,
            userStory: story.id,
        };
    }
    async updateIssue(ref, changes) {
        const operations = [];
        if (changes.title) {
            operations.push({ op: 'add', path: '/fields/System.Title', value: changes.title });
        }
        if (changes.description) {
            operations.push({ op: 'add', path: '/fields/System.Description', value: changes.description });
        }
        if (changes.status) {
            const adoState = await this.mapStatusToAdo(changes.status);
            operations.push({ op: 'add', path: '/fields/System.State', value: adoState });
        }
        if (operations.length > 0) {
            await this.apiRequest('PATCH', `/wit/workitems/${ref.id}?api-version=7.1`, operations, 'application/json-patch+json');
        }
    }
    async closeIssue(ref, comment) {
        const closedState = await this.getClosedState();
        const operations = [
            { op: 'add', path: '/fields/System.State', value: closedState },
        ];
        if (comment) {
            operations.push({ op: 'add', path: '/fields/System.History', value: comment });
        }
        const closeResponse = await this.apiRequest('PATCH', `/wit/workitems/${ref.id}?api-version=7.1`, operations, 'application/json-patch+json');
        if (!closeResponse.ok) {
            const text = await closeResponse.text();
            throw new Error(`Failed to close ADO work item ${ref.id}: ${closeResponse.status} ${text.substring(0, 200)}`);
        }
    }
    async reopenIssue(ref) {
        const activeState = await this.getActiveState();
        const reopenResponse = await this.apiRequest('PATCH', `/wit/workitems/${ref.id}?api-version=7.1`, [{ op: 'add', path: '/fields/System.State', value: activeState }], 'application/json-patch+json');
        if (!reopenResponse.ok) {
            const text = await reopenResponse.text();
            throw new Error(`Failed to reopen ADO work item ${ref.id}: ${reopenResponse.status} ${text.substring(0, 200)}`);
        }
    }
    async pullChanges(since) {
        const escapedProject = this.project.replace(/'/g, "''");
        let whereClause = `[System.TeamProject] = '${escapedProject}' AND [System.Tags] CONTAINS 'specweave'`;
        if (since) {
            const dateStr = since.toISOString().split('T')[0];
            whereClause += ` AND [System.ChangedDate] >= '${dateStr}'`;
        }
        const wiql = `SELECT [System.Id] FROM WorkItems WHERE ${whereClause} ORDER BY [System.ChangedDate] DESC`;
        const response = await this.apiRequest('POST', `/wit/wiql?api-version=7.1`, { query: wiql });
        if (!response.ok) {
            const text = await response.text();
            throw new Error(`Failed to query ADO work items: ${response.status} ${text.substring(0, 200)}`);
        }
        const data = await response.json();
        if (!data.workItems || data.workItems.length === 0)
            return [];
        // Batch work item fetches in groups of 200 (ADO API limit)
        const allIds = data.workItems.map(w => w.id);
        const batchSize = 200;
        const allItems = [];
        for (let i = 0; i < allIds.length; i += batchSize) {
            const batchIds = allIds.slice(i, i + batchSize);
            const itemsResponse = await this.apiRequest('GET', `/wit/workitems?ids=${batchIds.join(',')}&$expand=all&api-version=7.1`);
            if (!itemsResponse.ok) {
                const text = await itemsResponse.text();
                throw new Error(`Failed to fetch ADO work items batch: ${itemsResponse.status} ${text.substring(0, 200)}`);
            }
            const items = await itemsResponse.json();
            allItems.push(...(items.value || []));
        }
        const changes = [];
        for (const item of allItems) {
            const title = String(item.fields['System.Title'] || '');
            const state = String(item.fields['System.State'] || '');
            const isClosed = ADO_CLOSED_STATES.includes(state);
            const htmlUrl = item._links?.html?.href ||
                `https://dev.azure.com/${this.organization}/${this.project}/_workitems/edit/${item.id}`;
            changes.push({
                type: isClosed ? 'closed' : 'status_change',
                ref: {
                    platform: 'ado',
                    id: String(item.id),
                    url: htmlUrl,
                    userStory: this.extractUsId(title) || '',
                },
                data: {
                    status: state,
                    title,
                    tags: String(item.fields['System.Tags'] || '').split(';').map(t => t.trim()).filter(Boolean),
                },
            });
        }
        return changes;
    }
    async getIssueState(ref) {
        const response = await this.apiRequest('GET', `/wit/workitems/${ref.id}?api-version=7.1`);
        if (!response.ok) {
            const text = await response.text();
            throw new Error(`Failed to get work item ${ref.id}: ${response.status} ${text.substring(0, 200)}`);
        }
        const item = await response.json();
        const tags = String(item.fields['System.Tags'] || '').split(';').map(t => t.trim()).filter(Boolean);
        return {
            status: String(item.fields['System.State'] || ''),
            labels: tags,
            title: String(item.fields['System.Title'] || ''),
        };
    }
    async applyLabels(ref, labels) {
        const tags = labels.map(l => l.name).join('; ');
        await this.apiRequest('PATCH', `/wit/workitems/${ref.id}?api-version=7.1`, [{ op: 'add', path: '/fields/System.Tags', value: tags }], 'application/json-patch+json');
    }
    async detectHierarchy() {
        try {
            const response = await this.apiRequest('GET', `/wit/workitemtypes?api-version=7.1`);
            const data = await response.json();
            const types = (data.value || []).map(t => t.name.toLowerCase());
            const hasEpic = types.includes('epic');
            const hasFeature = types.includes('feature');
            const hasUserStory = types.includes('user story');
            const hasTask = types.includes('task');
            if (hasEpic && hasFeature && hasUserStory) {
                return {
                    pattern: 'standard',
                    levels: [
                        { externalType: 'Epic', specweaveMapping: 'skip' },
                        { externalType: 'Feature', specweaveMapping: 'feature' },
                        { externalType: 'User Story', specweaveMapping: 'user-story' },
                        { externalType: 'Task', specweaveMapping: 'task' },
                    ],
                };
            }
            if (hasTask && !hasUserStory && !hasFeature) {
                return {
                    pattern: 'flat',
                    levels: [{ externalType: 'Task', specweaveMapping: 'user-story' }],
                };
            }
            return {
                pattern: 'standard',
                levels: [
                    { externalType: 'Feature', specweaveMapping: 'feature' },
                    { externalType: 'User Story', specweaveMapping: 'user-story' },
                    { externalType: 'Task', specweaveMapping: 'task' },
                ],
            };
        }
        catch {
            return {
                pattern: 'flat',
                levels: [{ externalType: 'Task', specweaveMapping: 'user-story' }],
            };
        }
    }
    async reconcile(items) {
        const result = { created: 0, updated: 0, closed: 0, errors: [] };
        for (const item of items) {
            try {
                const state = await this.getIssueState(item.ref);
                const shouldBeClosed = item.expected.status === 'completed' || item.expected.status === 'abandoned';
                const isClosed = ADO_CLOSED_STATES.includes(state.status);
                if (shouldBeClosed && !isClosed) {
                    await this.closeIssue(item.ref, 'Auto-closed by SpecWeave reconciliation');
                    result.closed++;
                }
                else if (!shouldBeClosed && isClosed) {
                    await this.reopenIssue(item.ref);
                    result.updated++;
                }
            }
            catch (error) {
                result.errors.push(`Failed to reconcile ${item.ref.id}: ${String(error)}`);
            }
        }
        return result;
    }
    generateLabels(config) {
        return generateLabels(config);
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    /**
     * Lazy-cached process template detection.
     * Calls ADO project capabilities API on first invocation, caches result.
     * Returns undefined if detection fails (no throw).
     */
    async getTemplate() {
        if (this.templateDetected)
            return this.cachedTemplate;
        this.templateDetected = true;
        try {
            const response = await this.apiRequest('GET', `https://dev.azure.com/${this.organization}/_apis/projects/${this.project}?includeCapabilities=true&api-version=7.1`);
            if (!response.ok)
                return undefined;
            const data = await response.json();
            const templateName = data.capabilities?.processTemplate?.templateName;
            if (!templateName)
                return undefined;
            // Normalize template name to known types
            const normalized = this.normalizeTemplateName(templateName);
            if (normalized) {
                this.cachedTemplate = { template: normalized };
            }
            return this.cachedTemplate;
        }
        catch {
            return undefined;
        }
    }
    normalizeTemplateName(name) {
        const lower = name.toLowerCase();
        if (lower.includes('basic'))
            return 'Basic';
        if (lower.includes('agile'))
            return 'Agile';
        if (lower.includes('scrum'))
            return 'Scrum';
        if (lower.includes('cmmi'))
            return 'CMMI';
        if (lower.includes('safe'))
            return 'SAFe';
        return undefined;
    }
    async getClosedState() {
        if (this.config.closedState)
            return this.config.closedState;
        const template = await this.getTemplate();
        if (template)
            return TEMPLATE_CLOSED_MAP[template.template] ?? 'Closed';
        return 'Closed';
    }
    async getActiveState() {
        if (this.config.activeState)
            return this.config.activeState;
        const template = await this.getTemplate();
        if (template)
            return TEMPLATE_ACTIVE_MAP[template.template] ?? 'Active';
        return 'Active';
    }
    buildDescription(story, feature) {
        const lines = [];
        lines.push(`<b>Feature</b>: ${feature.id} — ${feature.title}<br/>`);
        if (story.description)
            lines.push(`<p>${story.description}</p>`);
        lines.push('<hr/><em>Auto-synced from SpecWeave</em>');
        return lines.join('\n');
    }
    async mapStatusToAdo(status) {
        const lower = status.toLowerCase();
        // For terminal states, resolve via template
        if (lower === 'completed' || lower === 'done') {
            return await this.getClosedState();
        }
        if (lower === 'abandoned')
            return 'Removed';
        // For active states, resolve via template
        if (lower === 'active') {
            return await this.getActiveState();
        }
        const staticMap = {
            planning: 'New',
            backlog: 'New',
        };
        return staticMap[lower] || 'Active';
    }
    extractUsId(title) {
        const match = title.match(/^(US-\d+[GJAE]?):/);
        return match ? match[1] : null;
    }
    async apiRequest(method, path, body, contentType) {
        const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
        const auth = Buffer.from(`:${this.pat}`).toString('base64');
        const headers = {
            Authorization: `Basic ${auth}`,
            Accept: 'application/json',
            'User-Agent': 'SpecWeave-Sync',
        };
        const options = { method, headers };
        if (body) {
            headers['Content-Type'] = contentType || 'application/json';
            options.body = JSON.stringify(body);
        }
        return fetch(url, options);
    }
}
//# sourceMappingURL=ado.js.map