/**
 * External Tool Resolver Service (v1.0.31+ - ADR-0211)
 *
 * Single source of truth for external tool selection.
 * Resolves which sync profile an increment should use based on:
 * 1. Explicit syncTarget in metadata (highest priority)
 * 2. Project mapping from spec.md **Project**: field
 * 3. Default profile from config (fallback)
 *
 * @module core/sync/external-tool-resolver
 */
import * as fs from '../../utils/fs-native.js';
import * as path from 'path';
import { consoleLogger } from '../../utils/logger.js';
/**
 * External Tool Resolver
 *
 * Provides unified external tool selection logic.
 * Use this service instead of scattered logic throughout the codebase.
 */
export class ExternalToolResolver {
    constructor(projectRoot, options = {}) {
        this.config = null;
        this.projectRoot = projectRoot;
        this.configPath = path.join(projectRoot, '.specweave', 'config.json');
        this.logger = options.logger ?? consoleLogger;
    }
    // ==========================================================================
    // Config Loading
    // ==========================================================================
    /**
     * Load configuration from disk
     */
    async loadConfig() {
        if (this.config) {
            return this.config;
        }
        try {
            const content = await fs.readFile(this.configPath, 'utf-8');
            this.config = JSON.parse(content);
            return this.config;
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                this.config = {};
                return this.config;
            }
            throw error;
        }
    }
    /**
     * Get sync configuration
     */
    async getSyncConfig() {
        const config = await this.loadConfig();
        return config.sync || null;
    }
    /**
     * Get workspace configuration
     */
    async getWorkspaceConfig() {
        const config = await this.loadConfig();
        return config.workspace || null;
    }
    /**
     * Convert WorkspaceRepoSync to ProjectMapping-compatible format for profile matching
     */
    convertWorkspaceSyncToMapping(sync) {
        const mapping = {};
        if (sync.github) {
            mapping.github = { owner: sync.github.owner, repo: sync.github.repo };
        }
        if (sync.jira) {
            mapping.jira = { project: sync.jira.projectKey };
        }
        if (sync.ado) {
            mapping.ado = { project: sync.ado.project };
            if (sync.ado.organization) {
                mapping.ado.organization = sync.ado.organization;
            }
        }
        return mapping;
    }
    // ==========================================================================
    // Resolution Methods
    // ==========================================================================
    /**
     * Resolve external tool for an increment
     *
     * Resolution priority:
     * 1. Explicit syncTarget in metadata
     * 2. Project mapping from spec.md **Project**: field
     * 3. Default profile from config
     *
     * @param incrementId - Increment ID (e.g., "0142-feature")
     * @param projectId - Optional project ID from spec.md (for priority 2)
     * @returns Resolution result with sync target and profile
     */
    async resolveForIncrement(incrementId, projectId) {
        const resolutionPath = [];
        const warnings = [];
        // Step 1: Check for explicit syncTarget in metadata
        const metadata = await this.loadIncrementMetadata(incrementId);
        if (metadata?.syncTarget) {
            resolutionPath.push(`metadata.syncTarget (profileId: ${metadata.syncTarget.profileId})`);
            const profile = await this.getProfileById(metadata.syncTarget.profileId);
            if (profile) {
                return {
                    syncTarget: metadata.syncTarget,
                    profile,
                    resolutionPath,
                    warnings,
                };
            }
            else {
                warnings.push(`Profile '${metadata.syncTarget.profileId}' not found in config`);
                resolutionPath.push('profile not found - falling through');
            }
        }
        // Step 2: Resolve from project ID (if provided)
        if (projectId) {
            const projectResult = await this.resolveForProject(projectId);
            if (projectResult.syncTarget) {
                resolutionPath.push(`workspace.repos['${projectId}'] → ${projectResult.syncTarget.profileId}`);
                return {
                    ...projectResult,
                    resolutionPath: [...resolutionPath, ...projectResult.resolutionPath],
                    warnings: [...warnings, ...projectResult.warnings],
                };
            }
            resolutionPath.push(`workspace.repos['${projectId}'] not found`);
        }
        // Step 3: Fall back to default profile
        const syncConfig = await this.getSyncConfig();
        if (syncConfig?.defaultProfile) {
            const profile = syncConfig.profiles?.[syncConfig.defaultProfile];
            if (profile) {
                resolutionPath.push(`config.sync.defaultProfile → ${syncConfig.defaultProfile}`);
                const syncTarget = {
                    profileId: syncConfig.defaultProfile,
                    provider: profile.provider,
                    derivedFrom: 'default-profile',
                    setAt: new Date().toISOString(),
                };
                return {
                    syncTarget,
                    profile,
                    resolutionPath,
                    warnings,
                };
            }
            warnings.push(`Default profile '${syncConfig.defaultProfile}' not found`);
        }
        // Step 4 (v1.0.46 FIX): Fallback to first available profile if no defaultProfile set
        // This matches GitHubProfileManager.getDefaultProfile() behavior
        if (syncConfig?.profiles) {
            const profileIds = Object.keys(syncConfig.profiles);
            if (profileIds.length > 0) {
                const firstProfileId = profileIds[0];
                const firstProfile = syncConfig.profiles[firstProfileId];
                resolutionPath.push(`fallback to first profile → ${firstProfileId}`);
                const syncTarget = {
                    profileId: firstProfileId,
                    provider: firstProfile.provider,
                    derivedFrom: 'first-profile-fallback', // v1.0.46: Explicit fallback type for first profile
                    setAt: new Date().toISOString(),
                };
                return {
                    syncTarget,
                    profile: firstProfile,
                    resolutionPath,
                    warnings,
                };
            }
        }
        // No resolution possible
        resolutionPath.push('no external tool configured');
        return {
            syncTarget: null,
            profile: null,
            resolutionPath,
            warnings,
        };
    }
    /**
     * Resolve external tool for a specific project ID
     *
     * Looks up project in workspace.repos and resolves to a profile.
     * Falls back to workspace.rootRepo when projectId matches workspace.name.
     *
     * @param projectId - SpecWeave project ID (e.g., "frontend-app")
     * @returns Resolution result
     */
    async resolveForProject(projectId) {
        const resolutionPath = [];
        const warnings = [];
        const workspace = await this.getWorkspaceConfig();
        // Look up workspace repo by ID
        let sync = null;
        const repo = workspace?.repos?.find((r) => r.id === projectId);
        if (repo?.sync) {
            sync = repo.sync;
        }
        else if (workspace && projectId === workspace.name && workspace.rootRepo) {
            // rootRepo fallback when projectId matches workspace name (AC-US8-02)
            sync = workspace.rootRepo;
        }
        if (!sync) {
            return {
                syncTarget: null,
                profile: null,
                resolutionPath: [`workspace.repos['${projectId}'] not found`],
                warnings,
            };
        }
        // Convert workspace sync to mapping format and find matching profile
        const mapping = this.convertWorkspaceSyncToMapping(sync);
        const providers = ['github', 'jira', 'ado'];
        for (const provider of providers) {
            const providerMapping = mapping[provider];
            if (providerMapping) {
                resolutionPath.push(`workspace.repos['${projectId}'].sync.${provider}`);
                const matchingProfile = await this.findProfileByMapping(provider, providerMapping);
                if (matchingProfile) {
                    const syncTarget = {
                        profileId: matchingProfile.id,
                        provider,
                        derivedFrom: 'project-mapping',
                        setAt: new Date().toISOString(),
                        sourceProjectId: projectId,
                    };
                    return {
                        syncTarget,
                        profile: matchingProfile.profile,
                        resolutionPath,
                        warnings,
                    };
                }
                // Sync config exists but no matching profile
                warnings.push(`No profile matches ${provider} config for project '${projectId}'`);
            }
        }
        return {
            syncTarget: null,
            profile: null,
            resolutionPath,
            warnings,
        };
    }
    /**
     * Resolve for a specific provider (e.g., "get me the GitHub profile")
     *
     * @param provider - Provider type
     * @param projectId - Optional project ID for project-specific profile
     * @returns Resolution result
     */
    async resolveForProvider(provider, projectId) {
        const resolutionPath = [];
        const warnings = [];
        // If projectId provided, try workspace repo sync first
        if (projectId) {
            const workspace = await this.getWorkspaceConfig();
            const repo = workspace?.repos?.find((r) => r.id === projectId);
            if (repo?.sync) {
                const mapping = this.convertWorkspaceSyncToMapping(repo.sync);
                const providerMapping = mapping[provider];
                if (providerMapping) {
                    const matchingProfile = await this.findProfileByMapping(provider, providerMapping);
                    if (matchingProfile) {
                        resolutionPath.push(`workspace.repos['${projectId}'].sync.${provider}`);
                        return {
                            syncTarget: {
                                profileId: matchingProfile.id,
                                provider,
                                derivedFrom: 'project-mapping',
                                setAt: new Date().toISOString(),
                                sourceProjectId: projectId,
                            },
                            profile: matchingProfile.profile,
                            resolutionPath,
                            warnings,
                        };
                    }
                }
            }
        }
        // Find any profile of this provider type
        const syncConfig = await this.getSyncConfig();
        if (syncConfig?.profiles) {
            for (const [profileId, profile] of Object.entries(syncConfig.profiles)) {
                if (profile.provider === provider) {
                    resolutionPath.push(`first ${provider} profile → ${profileId}`);
                    return {
                        syncTarget: {
                            profileId,
                            provider,
                            derivedFrom: 'default-profile',
                            setAt: new Date().toISOString(),
                        },
                        profile,
                        resolutionPath,
                        warnings,
                    };
                }
            }
        }
        resolutionPath.push(`no ${provider} profile configured`);
        return {
            syncTarget: null,
            profile: null,
            resolutionPath,
            warnings,
        };
    }
    // ==========================================================================
    // Validation Methods
    // ==========================================================================
    /**
     * Validate external tool configuration for an increment
     *
     * Call this at increment start to fail fast if config is missing.
     *
     * @param incrementId - Increment ID
     * @param projectId - Project ID from spec.md
     * @returns Validation result
     */
    async validateIncrementConfig(incrementId, projectId) {
        const errors = [];
        const warnings = [];
        const suggestions = [];
        // Resolve the sync target
        const resolution = await this.resolveForIncrement(incrementId, projectId);
        if (!resolution.syncTarget) {
            // Check what's missing
            const syncConfig = await this.getSyncConfig();
            const workspace = await this.getWorkspaceConfig();
            if (!syncConfig || Object.keys(syncConfig.profiles || {}).length === 0) {
                errors.push('No sync profiles configured');
                suggestions.push('Run `specweave init` to configure external tool integration');
            }
            else if (projectId && !workspace?.repos?.some((r) => r.id === projectId)) {
                errors.push(`No workspace repo for '${projectId}'`);
                suggestions.push(`Add repo in config.json: workspace.repos with id '${projectId}'`);
            }
            else if (!syncConfig.defaultProfile) {
                errors.push('No default sync profile configured');
                suggestions.push('Set config.sync.defaultProfile in config.json');
            }
            return {
                valid: false,
                errors,
                warnings: [...warnings, ...resolution.warnings],
                suggestions,
            };
        }
        // Validate the resolved profile exists and has required config
        if (!resolution.profile) {
            errors.push(`Profile '${resolution.syncTarget.profileId}' not found`);
            return {
                valid: false,
                syncTarget: resolution.syncTarget,
                errors,
                warnings: [...warnings, ...resolution.warnings],
                suggestions: ['Check config.sync.profiles in config.json'],
            };
        }
        // Validate provider-specific config
        const configErrors = this.validateProfileConfig(resolution.profile);
        if (configErrors.length > 0) {
            errors.push(...configErrors);
        }
        // Check for authentication
        const authWarnings = await this.checkAuthentication(resolution.profile.provider);
        warnings.push(...authWarnings);
        return {
            valid: errors.length === 0,
            syncTarget: resolution.syncTarget,
            errors,
            warnings: [...warnings, ...resolution.warnings],
            suggestions,
        };
    }
    /**
     * Validate profile configuration completeness
     */
    validateProfileConfig(profile) {
        const errors = [];
        const config = profile.config;
        switch (profile.provider) {
            case 'github': {
                const ghConfig = config;
                if (!ghConfig.owner) {
                    errors.push('GitHub config missing: owner');
                }
                if (!ghConfig.repo && !ghConfig.repos?.length) {
                    errors.push('GitHub config missing: repo or repos[]');
                }
                break;
            }
            case 'jira': {
                const jiraConfig = config;
                if (!jiraConfig.domain) {
                    errors.push('JIRA config missing: domain');
                }
                if (!jiraConfig.projectKey && !jiraConfig.projects?.length) {
                    errors.push('JIRA config missing: projectKey or projects[]');
                }
                break;
            }
            case 'ado': {
                const adoConfig = config;
                if (!adoConfig.organization) {
                    errors.push('ADO config missing: organization');
                }
                if (!adoConfig.project && !adoConfig.projects?.length) {
                    errors.push('ADO config missing: project or projects[]');
                }
                break;
            }
        }
        return errors;
    }
    /**
     * Check if authentication is configured for a provider
     */
    async checkAuthentication(provider) {
        const warnings = [];
        // Check .env file for tokens
        const envPath = path.join(this.projectRoot, '.env');
        let envContent = '';
        try {
            envContent = await fs.readFile(envPath, 'utf-8');
        }
        catch {
            // No .env file
        }
        switch (provider) {
            case 'github':
                if (!envContent.includes('GITHUB_TOKEN') && !envContent.includes('GH_TOKEN')) {
                    warnings.push('GitHub token not found in .env (GITHUB_TOKEN or GH_TOKEN)');
                }
                break;
            case 'jira':
                if (!envContent.includes('JIRA_API_TOKEN')) {
                    warnings.push('JIRA API token not found in .env (JIRA_API_TOKEN)');
                }
                if (!envContent.includes('JIRA_EMAIL')) {
                    warnings.push('JIRA email not found in .env (JIRA_EMAIL)');
                }
                break;
            case 'ado':
                if (!envContent.includes('AZURE_DEVOPS_PAT')) {
                    warnings.push('Azure DevOps PAT not found in .env (AZURE_DEVOPS_PAT)');
                }
                break;
        }
        return warnings;
    }
    // ==========================================================================
    // Helper Methods
    // ==========================================================================
    /**
     * Load increment metadata
     */
    async loadIncrementMetadata(incrementId) {
        const basePath = path.join(this.projectRoot, '.specweave', 'increments');
        const paths = [
            path.join(basePath, incrementId, 'metadata.json'),
            path.join(basePath, '_archive', incrementId, 'metadata.json'),
            path.join(basePath, '_paused', incrementId, 'metadata.json'),
        ];
        for (const metadataPath of paths) {
            try {
                return JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
            }
            catch {
                // Try next path
            }
        }
        return null;
    }
    /**
     * Get profile by ID
     */
    async getProfileById(profileId) {
        return (await this.getSyncConfig())?.profiles?.[profileId] ?? null;
    }
    /**
     * Find profile matching a project mapping
     */
    async findProfileByMapping(provider, mapping) {
        const syncConfig = await this.getSyncConfig();
        if (!syncConfig?.profiles)
            return null;
        for (const [profileId, profile] of Object.entries(syncConfig.profiles)) {
            if (profile.provider !== provider)
                continue;
            // Match by config properties
            const config = profile.config;
            switch (provider) {
                case 'github': {
                    const ghConfig = config;
                    const ghMapping = mapping;
                    if (ghConfig.owner === ghMapping.owner &&
                        (ghConfig.repo === ghMapping.repo || ghConfig.repos?.includes(ghMapping.repo))) {
                        return { id: profileId, profile };
                    }
                    break;
                }
                case 'jira': {
                    const jiraConfig = config;
                    const jiraMapping = mapping;
                    if (jiraConfig.projectKey === jiraMapping.project ||
                        jiraConfig.projects?.includes(jiraMapping.project)) {
                        return { id: profileId, profile };
                    }
                    break;
                }
                case 'ado': {
                    const adoConfig = config;
                    const adoMapping = mapping;
                    if (adoConfig.project === adoMapping.project ||
                        adoConfig.projects?.includes(adoMapping.project)) {
                        return { id: profileId, profile };
                    }
                    break;
                }
            }
        }
        return null;
    }
    /**
     * Create a sync target for saving to metadata
     *
     * @param profileId - Profile ID
     * @param provider - Provider type
     * @param derivedFrom - How it was determined
     * @param sourceProjectId - Optional source project
     * @returns SyncTarget object
     */
    static createSyncTarget(profileId, provider, derivedFrom, sourceProjectId) {
        return {
            profileId,
            provider,
            derivedFrom,
            setAt: new Date().toISOString(),
            sourceProjectId,
        };
    }
    // ==========================================================================
    // Utility Methods for Callers
    // ==========================================================================
    /**
     * Check if any external tool is configured
     */
    async hasExternalToolConfigured() {
        const syncConfig = await this.getSyncConfig();
        return Object.keys(syncConfig?.profiles ?? {}).length > 0;
    }
    /**
     * Get all configured providers
     */
    async getConfiguredProviders() {
        const profiles = (await this.getSyncConfig())?.profiles ?? {};
        return [...new Set(Object.values(profiles).map((p) => p.provider))];
    }
    /**
     * Get summary of external tool configuration
     */
    async getConfigSummary() {
        const syncConfig = await this.getSyncConfig();
        const workspace = await this.getWorkspaceConfig();
        const profiles = Object.values(syncConfig?.profiles ?? {});
        return {
            hasConfig: profiles.length > 0,
            providers: [...new Set(profiles.map((p) => p.provider))],
            profileCount: profiles.length,
            defaultProfile: syncConfig?.defaultProfile,
            projectMappingCount: workspace?.repos?.length ?? 0,
        };
    }
}
/**
 * Factory function for creating ExternalToolResolver
 */
export function createExternalToolResolver(projectRoot, logger) {
    return new ExternalToolResolver(projectRoot, { logger });
}
//# sourceMappingURL=external-tool-resolver.js.map