/**
 * JIRA Reconciler (NEW in v0.33.0)
 *
 * Reconciles JIRA issue states with increment metadata.json statuses.
 * Fixes drift between local SpecWeave state and JIRA:
 * - Closes issues for completed increments that are still open
 * - Reopens issues for in-progress increments that are closed
 *
 * Mirrors the GitHub reconciler pattern for consistency.
 *
 * Triggered by:
 * - sw:jira-reconcile command (manual)
 * - SessionStart hook (automatic, if configured)
 * - post-increment-status-change.sh (on resume/abandon)
 */
import { promises as fs, existsSync } from 'fs';
import path from 'path';
import { consoleLogger } from '../utils/logger.js';
import { ConfigManager } from '../core/config/config-manager.js';
import { resolvePermissions } from './config.js';
import { deriveFeatureId } from '../utils/feature-id-derivation.js';
/**
 * JIRA Status Mapping
 */
const JIRA_STATUSES = {
    CLOSED: ['Done', 'Closed', 'Resolved', "Won't Do"],
    OPEN: ['To Do', 'In Progress', 'In Review', 'Open', 'Reopened'],
};
export class JiraReconciler {
    constructor(options) {
        this.domain = '';
        this.auth = '';
        this.projectRoot = options.projectRoot;
        this.dryRun = options.dryRun ?? false;
        this.logger = options.logger ?? consoleLogger;
        this.configManager = new ConfigManager(options.projectRoot);
    }
    /**
     * Main reconciliation entry point
     */
    async reconcile() {
        const result = {
            scanned: 0,
            mismatches: 0,
            closed: 0,
            reopened: 0,
            errors: [],
            details: [],
        };
        try {
            // 1. Check if JIRA sync is enabled
            const config = await this.loadConfig();
            // v1.0.240 FIX: Use resolvePermissions() to honor "bidirectional" preset
            const syncAny = config.sync;
            const permissions = resolvePermissions(syncAny?.preset, undefined, config.sync?.settings);
            const canUpdate = config.sync?.settings?.canUpdateExternalItems ?? permissions.canUpsert;
            const canUpdateStatus = config.sync?.settings?.canUpdateStatus ?? permissions.canUpdateStatus;
            const jiraEnabled = config.sync?.jira?.enabled ?? false;
            if (!canUpdate || !jiraEnabled) {
                this.logger.log('ℹ️  JIRA sync is disabled - skipping reconciliation');
                this.logger.log('   Enable with: canUpdateExternalItems=true AND sync.jira.enabled=true');
                return result;
            }
            if (!canUpdateStatus) {
                this.logger.log('ℹ️  JIRA status updates are disabled - skipping reconciliation');
                this.logger.log('   Enable with: canUpdateStatus=true');
                return result;
            }
            // 2. Verify JIRA credentials (secrets from .env, domain from config.json)
            if (!(await this.initCredentials())) {
                result.errors.push('JIRA credentials not configured');
                this.logger.error('❌ JIRA credentials not found. Set JIRA_API_TOKEN and JIRA_EMAIL in .env, and issueTracker.domain in config.json.');
                return result;
            }
            // 3. Scan all non-archived increments
            const increments = await this.scanIncrements();
            result.scanned = increments.length;
            this.logger.log(`\n📊 Scanning ${increments.length} increment(s) for JIRA state drift...\n`);
            // 4. Check and fix each increment
            for (const inc of increments) {
                await this.reconcileIncrement(inc, result);
            }
            // 5. Report summary
            this.logger.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
            this.logger.log('📊 JIRA RECONCILIATION SUMMARY');
            this.logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
            this.logger.log(`   Increments scanned:  ${result.scanned}`);
            this.logger.log(`   Mismatches found:    ${result.mismatches}`);
            this.logger.log(`   Issues closed:       ${result.closed}`);
            this.logger.log(`   Issues reopened:     ${result.reopened}`);
            this.logger.log(`   Errors:              ${result.errors.length}`);
            if (this.dryRun) {
                this.logger.log('\n   ⚠️  DRY RUN - No changes were made');
            }
            this.logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
            return result;
        }
        catch (error) {
            result.errors.push(`Reconciliation error: ${error.message}`);
            this.logger.error('❌ Reconciliation failed:', error.message);
            return result;
        }
    }
    /**
     * Reconcile a single increment
     */
    async reconcileIncrement(inc, result) {
        const status = inc.metadataStatus;
        // Determine expected JIRA state
        const shouldBeClosed = status === 'completed' || status === 'abandoned';
        const shouldBeOpen = status === 'active' || status === 'planning' || status === 'backlog' || status === 'ready_for_review' || status === 'paused';
        if (!shouldBeClosed && !shouldBeOpen) {
            this.logger.log(`  ⚠️ Unknown increment status '${status}' for ${inc.incrementId} — skipping`);
            return;
        }
        // Check main issue
        if (inc.issue) {
            await this.reconcileIssue(inc.incrementId, inc.issue.key, shouldBeClosed, shouldBeOpen, status, result);
        }
        // Check User Story issues
        for (const us of inc.userStoryIssues) {
            await this.reconcileIssue(`${inc.incrementId}/${us.userStoryId}`, us.issueKey, shouldBeClosed, shouldBeOpen, status, result);
        }
    }
    /**
     * Reconcile a single issue
     */
    async reconcileIssue(context, issueKey, shouldBeClosed, shouldBeOpen, metadataStatus, result) {
        try {
            // Get current JIRA state
            const issue = await this.getIssue(issueKey);
            const currentStatus = issue?.fields?.status?.name ?? 'Unknown';
            const isCurrentlyClosed = JIRA_STATUSES.CLOSED.includes(currentStatus);
            // Check for mismatch
            if (shouldBeClosed && !isCurrentlyClosed) {
                // Should be closed but is open
                result.mismatches++;
                this.logger.log(`  ❌ Issue ${issueKey} (${context}): ${currentStatus} but should be CLOSED (status=${metadataStatus})`);
                if (!this.dryRun) {
                    const comment = `h2. 🔄 Auto-Reconciled by SpecWeave

This issue was closed by SpecWeave reconciliation.

*Reason*: Increment status is \`${metadataStatus}\` but JIRA issue was still open.

----
🤖 Auto-reconciled by SpecWeave`;
                    await this.transitionIssue(issueKey, 'Done', comment);
                    result.closed++;
                    this.logger.log(`     ✅ Closed issue ${issueKey}`);
                }
                else {
                    this.logger.log(`     [DRY RUN] Would close issue ${issueKey}`);
                }
                result.details.push({
                    incrementId: context,
                    action: this.dryRun ? 'skip' : 'close',
                    issueKey,
                    reason: `Status=${metadataStatus}, JIRA=${currentStatus}`,
                });
            }
            else if (shouldBeOpen && isCurrentlyClosed) {
                // Should be open but is closed
                result.mismatches++;
                this.logger.log(`  ❌ Issue ${issueKey} (${context}): CLOSED but should be OPEN (status=${metadataStatus})`);
                if (!this.dryRun) {
                    const comment = `h2. 🔄 Auto-Reconciled by SpecWeave

This issue was reopened by SpecWeave reconciliation.

*Reason*: Increment status is \`${metadataStatus}\` but JIRA issue was closed.
This typically happens when an increment was resumed after being paused/completed.

----
🤖 Auto-reconciled by SpecWeave`;
                    await this.transitionIssue(issueKey, 'To Do', comment);
                    result.reopened++;
                    this.logger.log(`     ✅ Reopened issue ${issueKey}`);
                }
                else {
                    this.logger.log(`     [DRY RUN] Would reopen issue ${issueKey}`);
                }
                result.details.push({
                    incrementId: context,
                    action: this.dryRun ? 'skip' : 'reopen',
                    issueKey,
                    reason: `Status=${metadataStatus}, JIRA=${currentStatus}`,
                });
            }
            else {
                // State matches
                this.logger.log(`  ✅ Issue ${issueKey} (${context}): State matches (${currentStatus})`);
                result.details.push({
                    incrementId: context,
                    action: 'skip',
                    issueKey,
                    reason: 'State matches',
                });
            }
        }
        catch (error) {
            result.errors.push(`Issue ${issueKey}: ${error.message}`);
            result.details.push({
                incrementId: context,
                action: 'error',
                issueKey,
                reason: error.message,
            });
        }
    }
    /**
     * Scan increments directory for non-archived increments
     */
    async scanIncrements() {
        const incrementsPath = path.join(this.projectRoot, '.specweave', 'increments');
        const results = [];
        if (!existsSync(incrementsPath)) {
            return results;
        }
        const entries = await fs.readdir(incrementsPath, { withFileTypes: true });
        for (const entry of entries) {
            if (!entry.isDirectory())
                continue;
            if (entry.name === '_archive' || entry.name.startsWith('.'))
                continue;
            const incrementPath = path.join(incrementsPath, entry.name);
            const metadataPath = path.join(incrementPath, 'metadata.json');
            if (!existsSync(metadataPath))
                continue;
            try {
                const metadataContent = await fs.readFile(metadataPath, 'utf-8');
                const metadata = JSON.parse(metadataContent);
                // Check for JIRA issue link (support both new and legacy naming)
                const jiraSync = metadata.external_sync?.jira || metadata.external_ids?.jira;
                if (!jiraSync?.issueKey && !jiraSync?.epic)
                    continue;
                // v1.0.240 FIX: Auto-derive featureId when metadata.feature_id is null
                let featureId = metadata.featureId || metadata.feature_id;
                if (!featureId) {
                    try {
                        featureId = deriveFeatureId(entry.name) || undefined;
                    }
                    catch { /* skip */ }
                }
                const state = {
                    incrementId: entry.name,
                    incrementPath,
                    metadataStatus: metadata.status || 'unknown',
                    featureId,
                    issue: {
                        key: jiraSync.issueKey || jiraSync.epic,
                        url: jiraSync.issueUrl,
                    },
                    userStoryIssues: [],
                };
                // Extract user story issues if present
                if (jiraSync.stories && Array.isArray(jiraSync.stories)) {
                    for (const story of jiraSync.stories) {
                        if (story.issueKey || story.key) {
                            state.userStoryIssues.push({
                                userStoryId: story.id || story.userStoryId,
                                issueKey: story.issueKey || story.key,
                            });
                        }
                    }
                }
                results.push(state);
            }
            catch {
                // Skip invalid metadata
            }
        }
        return results;
    }
    /**
     * Load config.json
     */
    async loadConfig() {
        const configPath = path.join(this.projectRoot, '.specweave', 'config.json');
        try {
            const content = await fs.readFile(configPath, 'utf-8');
            return JSON.parse(content);
        }
        catch {
            return {};
        }
    }
    /**
     * Initialize JIRA credentials
     * Reads secrets (token, email) from process.env
     * Reads config (domain) from ConfigManager (config.json)
     */
    async initCredentials() {
        // Secrets from .env (correct - these are sensitive)
        const token = process.env.JIRA_API_TOKEN;
        const email = process.env.JIRA_EMAIL;
        // Config from ConfigManager (correct - domain is not secret)
        const config = await this.configManager.read();
        this.domain = config.issueTracker?.domain || '';
        if (!token || !email || !this.domain) {
            return false;
        }
        this.auth = Buffer.from(`${email}:${token}`).toString('base64');
        return true;
    }
    /**
     * Get issue from JIRA API
     */
    async getIssue(issueKey) {
        const url = `https://${this.domain}/rest/api/3/issue/${issueKey}`;
        const response = await fetch(url, {
            headers: {
                Authorization: `Basic ${this.auth}`,
                Accept: 'application/json',
            },
        });
        if (!response.ok) {
            throw new Error(`Failed to get issue: ${response.status}`);
        }
        return response.json();
    }
    /**
     * Get available transitions for an issue
     */
    async getTransitions(issueKey) {
        const url = `https://${this.domain}/rest/api/3/issue/${issueKey}/transitions`;
        const response = await fetch(url, {
            headers: {
                Authorization: `Basic ${this.auth}`,
                Accept: 'application/json',
            },
        });
        if (!response.ok) {
            throw new Error(`Failed to get transitions: ${response.status}`);
        }
        const data = await response.json();
        return data.transitions || [];
    }
    /**
     * Transition issue to new status and add comment
     */
    async transitionIssue(issueKey, targetStatus, comment) {
        // First, add comment
        await this.addComment(issueKey, comment);
        // Get available transitions
        const transitions = await this.getTransitions(issueKey);
        // Find transition matching target status
        const transition = transitions.find((t) => t.name.toLowerCase() === targetStatus.toLowerCase() ||
            t.to?.name?.toLowerCase() === targetStatus.toLowerCase());
        if (!transition) {
            // Try common alternatives
            const alternatives = targetStatus === 'Done'
                ? ['Closed', 'Resolved', 'Complete']
                : ['Open', 'Reopened', 'In Progress'];
            const altTransition = transitions.find((t) => alternatives.some(alt => t.name.toLowerCase() === alt.toLowerCase() ||
                t.to?.name?.toLowerCase() === alt.toLowerCase()));
            if (!altTransition) {
                throw new Error(`No transition found to "${targetStatus}". Available: ${transitions.map((t) => t.name).join(', ')}`);
            }
            await this.doTransition(issueKey, altTransition.id);
        }
        else {
            await this.doTransition(issueKey, transition.id);
        }
    }
    /**
     * Execute transition
     */
    async doTransition(issueKey, transitionId) {
        const url = `https://${this.domain}/rest/api/3/issue/${issueKey}/transitions`;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                Authorization: `Basic ${this.auth}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                transition: { id: transitionId },
            }),
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Failed to transition issue: ${response.status} - ${error}`);
        }
    }
    /**
     * Add comment to issue
     */
    async addComment(issueKey, comment) {
        const url = `https://${this.domain}/rest/api/3/issue/${issueKey}/comment`;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                Authorization: `Basic ${this.auth}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                body: {
                    type: 'doc',
                    version: 1,
                    content: [
                        {
                            type: 'paragraph',
                            content: [
                                {
                                    type: 'text',
                                    text: comment,
                                },
                            ],
                        },
                    ],
                },
            }),
        });
        if (!response.ok) {
            // Non-fatal, just log warning
            this.logger.log(`     ⚠️  Failed to add comment to ${issueKey}`);
        }
    }
}
/**
 * Convenience function for quick reconciliation
 */
export async function reconcileJira(projectRoot = process.cwd(), dryRun = false) {
    const reconciler = new JiraReconciler({ projectRoot, dryRun });
    return reconciler.reconcile();
}
export default JiraReconciler;
//# sourceMappingURL=jira-reconciler.js.map