/**
 * Secure Credentials Manager
 *
 * Handles API keys and credentials for external integrations (Jira, ADO, etc.)
 *
 * ARCHITECTURE (v0.34.0+):
 * - SECRETS (PAT, API_TOKEN, EMAIL) -> .env file (gitignored)
 * - CONFIG (domain, organization, project) -> config.json via ConfigManager
 *
 * This class loads secrets from .env and provides fallback to process.env
 * for backward compatibility. Config values should be loaded from ConfigManager
 * in calling code (reconcilers, importers, etc.)
 *
 * NOTE: For config values (domain, org, project), use ConfigManager directly.
 * Only use CredentialsManager for secrets (PAT, tokens, emails).
 */
import * as fs from 'fs';
import * as path from 'path';
/**
 * Canonical ADO PAT environment-variable aliases (0865 T-006).
 *
 * SINGLE SOURCE OF TRUTH for the ADO secret surface. Both the runtime resolver
 * (`integrations/ado/ado-pat-provider.getAdoPat`) and the health check
 * (`cli/commands/sync-health`) read through this table so health can never
 * report green while runtime writes throw "PAT not found".
 *
 * Order is precedence: AZURE_DEVOPS_PAT wins over AZURE_DEVOPS_TOKEN over ADO_PAT.
 * Org-specific PATs (`AZURE_DEVOPS_PAT_{ORG}`) take precedence over all of these
 * and are resolved by the caller before consulting this table.
 */
export const ADO_PAT_ALIASES = [
    'AZURE_DEVOPS_PAT',
    'AZURE_DEVOPS_TOKEN',
    'ADO_PAT',
];
/**
 * Resolve the ADO PAT from environment, honoring the org-specific override and
 * the canonical alias table. Returns `undefined` when none is set (callers
 * decide whether that is fatal). SECURITY: never logs the value.
 *
 * @param organization - Optional ADO organization for the `AZURE_DEVOPS_PAT_{ORG}` override
 */
export function resolveAdoPat(organization) {
    if (organization) {
        const orgEnvKey = `AZURE_DEVOPS_PAT_${organization.toUpperCase().replace(/-/g, '_')}`;
        const orgPat = process.env[orgEnvKey];
        if (orgPat)
            return orgPat;
    }
    for (const alias of ADO_PAT_ALIASES) {
        const value = process.env[alias];
        if (value)
            return value;
    }
    return undefined;
}
export class CredentialsManager {
    constructor() {
        this.credentials = {};
        this.envLoaded = false;
        this.loadFromEnv();
    }
    /**
     * Parse comma-separated string into array of trimmed, non-empty strings
     */
    parseCommaSeparated(value) {
        if (!value)
            return [];
        return value.split(',').map(s => s.trim()).filter(s => s.length > 0);
    }
    static getInstance() {
        if (!CredentialsManager.instance) {
            CredentialsManager.instance = new CredentialsManager();
        }
        return CredentialsManager.instance;
    }
    /**
     * Load credentials from .env file or environment variables
     */
    loadFromEnv() {
        // First, try to load from .env file
        const envPath = path.join(process.cwd(), '.env');
        if (fs.existsSync(envPath)) {
            const envContent = fs.readFileSync(envPath, 'utf-8');
            const envVars = this.parseEnvFile(envContent);
            // Merge with process.env (process.env takes precedence)
            Object.keys(envVars).forEach(key => {
                if (!process.env[key]) {
                    process.env[key] = envVars[key];
                }
            });
        }
        // Load ADO credentials (SECRETS only - PAT)
        // NOTE: organization and project should come from config.json via ConfigManager
        if (process.env.AZURE_DEVOPS_PAT) {
            const singleTeam = process.env.AZURE_DEVOPS_TEAM || '';
            const teams = this.parseCommaSeparated(process.env.AZURE_DEVOPS_TEAMS);
            const effectiveTeams = teams.length > 0 ? teams : (singleTeam ? [singleTeam] : []);
            this.credentials.ado = {
                pat: process.env.AZURE_DEVOPS_PAT,
                organization: process.env.AZURE_DEVOPS_ORG || '',
                project: process.env.AZURE_DEVOPS_PROJECT || '',
                team: singleTeam || effectiveTeams[0],
                teams: effectiveTeams.length > 1 ? effectiveTeams : undefined,
            };
        }
        // Load Jira credentials (SECRETS only - token, email)
        // NOTE: domain should come from config.json via ConfigManager
        if (process.env.JIRA_API_TOKEN) {
            const singleProject = process.env.JIRA_PROJECT || '';
            const projects = this.parseCommaSeparated(process.env.JIRA_PROJECTS);
            const effectiveProjects = projects.length > 0 ? projects : (singleProject ? [singleProject] : []);
            const components = this.parseCommaSeparated(process.env.JIRA_COMPONENTS);
            const boards = this.parseCommaSeparated(process.env.JIRA_BOARDS);
            this.credentials.jira = {
                apiToken: process.env.JIRA_API_TOKEN,
                email: process.env.JIRA_EMAIL || '',
                domain: process.env.JIRA_DOMAIN || '',
                strategy: process.env.JIRA_STRATEGY || undefined,
                projects: effectiveProjects.length > 0 ? effectiveProjects : undefined,
                project: singleProject,
                components: components.length > 0 ? components : undefined,
                boards: boards.length > 0 ? boards : undefined,
            };
        }
        // Load GitHub credentials
        if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) {
            const repos = this.parseCommaSeparated(process.env.GITHUB_REPOS);
            const teams = this.parseCommaSeparated(process.env.GITHUB_TEAMS);
            // Parse JSON mapping
            let teamRepoMapping;
            const teamRepoMappingStr = process.env.GITHUB_TEAM_REPO_MAPPING;
            if (teamRepoMappingStr) {
                try {
                    teamRepoMapping = JSON.parse(teamRepoMappingStr);
                }
                catch (e) {
                    console.error('Failed to parse GITHUB_TEAM_REPO_MAPPING:', e);
                }
            }
            this.credentials.github = {
                token: process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '',
                strategy: process.env.GITHUB_STRATEGY || undefined,
                owner: process.env.GITHUB_OWNER || '',
                repos: repos.length > 0 ? repos : undefined,
                repo: process.env.GITHUB_REPO || '',
                teams: teams.length > 0 ? teams : undefined,
                teamRepoMapping,
            };
        }
        this.envLoaded = true;
    }
    /**
     * Parse .env file content into key-value pairs
     */
    parseEnvFile(content) {
        const result = {};
        const lines = content.split('\n');
        for (const line of lines) {
            const trimmed = line.trim();
            // Skip empty lines and comments
            if (!trimmed || trimmed.startsWith('#')) {
                continue;
            }
            // Parse KEY=VALUE
            const match = trimmed.match(/^([^=]+)=(.*)$/);
            if (match) {
                const key = match[1].trim();
                let value = match[2].trim();
                // Remove quotes if present
                if ((value.startsWith('"') && value.endsWith('"')) ||
                    (value.startsWith("'") && value.endsWith("'"))) {
                    value = value.slice(1, -1);
                }
                result[key] = value;
            }
        }
        return result;
    }
    /**
     * Get ADO credentials
     *
     * NOTE (v0.34.0+): This method returns secrets from .env.
     * For config values (organization, project), use ConfigManager directly:
     *   const config = await configManager.read();
     *   const org = config.issueTracker?.organization_ado || '';
     *
     * @throws Error if credentials not found
     */
    getAdoCredentials() {
        if (!this.credentials.ado) {
            throw new Error('Azure DevOps credentials not found. Please set AZURE_DEVOPS_PAT in .env file.\n' +
                'For organization and project, use .specweave/config.json (issueTracker.organization_ado).');
        }
        this.validateAdoCredentials(this.credentials.ado);
        return this.credentials.ado;
    }
    /**
     * Get Jira credentials
     *
     * NOTE (v0.34.0+): This method returns secrets from .env.
     * For config values (domain), use ConfigManager directly:
     *   const config = await configManager.read();
     *   const domain = config.issueTracker?.domain || '';
     *
     * @throws Error if credentials not found
     */
    getJiraCredentials() {
        if (!this.credentials.jira) {
            throw new Error('Jira credentials not found. Please set JIRA_API_TOKEN and JIRA_EMAIL in .env file.\n' +
                'For domain, use .specweave/config.json (issueTracker.domain).');
        }
        this.validateJiraCredentials(this.credentials.jira);
        return this.credentials.jira;
    }
    /**
     * Check if ADO credentials are available
     */
    hasAdoCredentials() {
        return !!this.credentials.ado?.pat;
    }
    /**
     * Check if Jira credentials are available
     */
    hasJiraCredentials() {
        return !!this.credentials.jira?.apiToken;
    }
    /**
     * Validate ADO credentials format
     */
    validateAdoCredentials(creds) {
        if (!creds.pat || creds.pat.length !== 52) {
            console.warn(`⚠️  Azure DevOps PAT length unexpected. Expected: 52 characters, Got: ${creds.pat.length}`);
        }
        if (!creds.organization || !/^[a-zA-Z0-9-]+$/.test(creds.organization)) {
            throw new Error(`Invalid Azure DevOps organization: "${creds.organization}". ` +
                'Expected: alphanumeric and hyphens only.');
        }
        if (!creds.project) {
            throw new Error('Azure DevOps project name is required.');
        }
    }
    /**
     * Validate Jira credentials format
     */
    validateJiraCredentials(creds) {
        const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
        if (!emailRegex.test(creds.email)) {
            throw new Error(`Invalid Jira email: "${creds.email}". Expected: valid email address.`);
        }
        if (!creds.domain.includes('.atlassian.net') && !creds.domain.includes('://')) {
            console.warn(`⚠️  Jira domain format unexpected: "${creds.domain}". ` +
                'Expected: *.atlassian.net or custom domain with protocol.');
        }
        if (!creds.apiToken) {
            throw new Error('Jira API token is required.');
        }
    }
    /**
     * Get masked version of credentials for logging (safe to display)
     */
    getMaskedAdoInfo() {
        if (!this.credentials.ado) {
            return 'ADO credentials: Not configured';
        }
        const lines = [
            'ADO credentials:',
            `  PAT: ${this.maskSecret(this.credentials.ado.pat)}`,
            `  Organization: ${this.credentials.ado.organization}`,
            `  Project: ${this.credentials.ado.project}`
        ];
        if (this.credentials.ado.teams && this.credentials.ado.teams.length > 1) {
            lines.push(`  Teams (${this.credentials.ado.teams.length}): ${this.credentials.ado.teams.join(', ')}`);
        }
        else if (this.credentials.ado.team) {
            lines.push(`  Team: ${this.credentials.ado.team}`);
        }
        return lines.join('\n');
    }
    /**
     * Get masked version of credentials for logging (safe to display)
     */
    getMaskedJiraInfo() {
        if (!this.credentials.jira) {
            return 'Jira credentials: Not configured';
        }
        const lines = [
            'Jira credentials:',
            `  API Token: ${this.maskSecret(this.credentials.jira.apiToken)}`,
            `  Email: ${this.credentials.jira.email}`,
            `  Domain: ${this.credentials.jira.domain}`
        ];
        if (this.credentials.jira.projects && this.credentials.jira.projects.length > 1) {
            lines.push(`  Projects (${this.credentials.jira.projects.length}): ${this.credentials.jira.projects.join(', ')}`);
        }
        else if (this.credentials.jira.project) {
            lines.push(`  Project: ${this.credentials.jira.project}`);
        }
        return lines.join('\n');
    }
    /**
     * Mask a secret for safe logging
     */
    maskSecret(secret) {
        if (!secret)
            return '[EMPTY]';
        if (secret.length <= 8)
            return '****';
        const visibleChars = 4;
        const start = secret.slice(0, visibleChars);
        const end = secret.slice(-visibleChars);
        return `${start}...${end} (${secret.length} chars)`;
    }
    /**
     * Save credentials to .env file (with backup)
     */
    saveToEnvFile(config) {
        const envPath = path.join(process.cwd(), '.env');
        // Backup existing .env
        if (fs.existsSync(envPath)) {
            const backup = `${envPath}.backup.${Date.now()}`;
            fs.copyFileSync(envPath, backup);
            console.log(`📦 Backed up existing .env to: ${backup}`);
        }
        // Read existing content
        let existingContent = '';
        let existingVars = {};
        if (fs.existsSync(envPath)) {
            existingContent = fs.readFileSync(envPath, 'utf-8');
            existingVars = this.parseEnvFile(existingContent);
        }
        // Update with new credentials
        if (config.ado) {
            existingVars.AZURE_DEVOPS_PAT = config.ado.pat;
            existingVars.AZURE_DEVOPS_ORG = config.ado.organization;
            existingVars.AZURE_DEVOPS_PROJECT = config.ado.project; // Always singular (one project)
            // Write teams as comma-separated if multiple, otherwise single
            if (config.ado.teams && config.ado.teams.length > 1) {
                existingVars.AZURE_DEVOPS_TEAMS = config.ado.teams.join(',');
                // Remove singular form to avoid confusion
                delete existingVars.AZURE_DEVOPS_TEAM;
            }
            else if (config.ado.team) {
                existingVars.AZURE_DEVOPS_TEAM = config.ado.team;
                // Remove plural form if only one team
                delete existingVars.AZURE_DEVOPS_TEAMS;
            }
        }
        if (config.jira) {
            existingVars.JIRA_API_TOKEN = config.jira.apiToken;
            existingVars.JIRA_EMAIL = config.jira.email;
            existingVars.JIRA_DOMAIN = config.jira.domain;
            // Write strategy if specified
            if (config.jira.strategy) {
                existingVars.JIRA_STRATEGY = config.jira.strategy;
            }
            // Strategy 1: Project-per-team (multiple projects)
            if (config.jira.strategy === 'project-per-team' && config.jira.projects) {
                existingVars.JIRA_PROJECTS = config.jira.projects.join(',');
                delete existingVars.JIRA_PROJECT;
                delete existingVars.JIRA_COMPONENTS;
                delete existingVars.JIRA_BOARDS;
            }
            // Strategy 2: Component-based (one project, multiple components)
            else if (config.jira.strategy === 'component-based') {
                if (config.jira.project) {
                    existingVars.JIRA_PROJECT = config.jira.project;
                }
                if (config.jira.components) {
                    existingVars.JIRA_COMPONENTS = config.jira.components.join(',');
                }
                delete existingVars.JIRA_PROJECTS;
                delete existingVars.JIRA_BOARDS;
            }
            // Strategy 3: Board-based (one project, board IDs)
            else if (config.jira.strategy === 'board-based') {
                if (config.jira.project) {
                    existingVars.JIRA_PROJECT = config.jira.project;
                }
                if (config.jira.boards) {
                    existingVars.JIRA_BOARDS = config.jira.boards.join(',');
                }
                delete existingVars.JIRA_PROJECTS;
                delete existingVars.JIRA_COMPONENTS;
            }
            // Legacy: No strategy (backward compatibility)
            else if (config.jira.projects && config.jira.projects.length > 1) {
                existingVars.JIRA_PROJECTS = config.jira.projects.join(',');
                delete existingVars.JIRA_PROJECT;
            }
            else if (config.jira.project) {
                existingVars.JIRA_PROJECT = config.jira.project;
                delete existingVars.JIRA_PROJECTS;
            }
        }
        if (config.github) {
            existingVars.GH_TOKEN = config.github.token;
            // Write strategy if specified
            if (config.github.strategy) {
                existingVars.GITHUB_STRATEGY = config.github.strategy;
            }
            // Write owner
            if (config.github.owner) {
                existingVars.GITHUB_OWNER = config.github.owner;
            }
            // Strategy 1: Repository-per-team
            if (config.github.strategy === 'repository-per-team' && config.github.repos) {
                existingVars.GITHUB_REPOS = config.github.repos.join(',');
                delete existingVars.GITHUB_REPO;
                delete existingVars.GITHUB_TEAMS;
                delete existingVars.GITHUB_TEAM_REPO_MAPPING;
            }
            // Strategy 2: Team-based (monorepo)
            else if (config.github.strategy === 'team-based') {
                if (config.github.repo) {
                    existingVars.GITHUB_REPO = config.github.repo;
                }
                if (config.github.teams) {
                    existingVars.GITHUB_TEAMS = config.github.teams.join(',');
                }
                delete existingVars.GITHUB_REPOS;
                delete existingVars.GITHUB_TEAM_REPO_MAPPING;
            }
            // Strategy 3: Team-multi-repo
            else if (config.github.strategy === 'team-multi-repo' && config.github.teamRepoMapping) {
                existingVars.GITHUB_TEAM_REPO_MAPPING = JSON.stringify(config.github.teamRepoMapping);
                delete existingVars.GITHUB_REPOS;
                delete existingVars.GITHUB_REPO;
                delete existingVars.GITHUB_TEAMS;
            }
        }
        // Write back to .env
        const newContent = Object.entries(existingVars)
            .map(([key, value]) => `${key}=${value}`)
            .join('\n');
        fs.writeFileSync(envPath, newContent + '\n', 'utf-8');
        console.log('✅ Credentials saved to .env');
        // Ensure .env is in .gitignore
        this.ensureGitignore();
        // Reload credentials
        this.loadFromEnv();
    }
    /**
     * Ensure .env is in .gitignore
     */
    ensureGitignore() {
        const gitignorePath = path.join(process.cwd(), '.gitignore');
        let gitignoreContent = '';
        if (fs.existsSync(gitignorePath)) {
            gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
        }
        if (!gitignoreContent.includes('.env')) {
            fs.appendFileSync(gitignorePath, '\n# Environment variables\n.env\n.env.*.backup\n');
            console.log('✅ Added .env to .gitignore');
        }
    }
    /**
     * Create .env.example file for team
     */
    createEnvExample() {
        const examplePath = path.join(process.cwd(), '.env.example');
        const exampleContent = `# Azure DevOps Personal Access Token
# Get from: https://dev.azure.com/{org}/_usersSettings/tokens
# Scopes: Work Items (Read, Write, Manage), Code (Read), Project (Read)
AZURE_DEVOPS_PAT=your-ado-pat-52-chars-base64
AZURE_DEVOPS_ORG=your-organization-name
AZURE_DEVOPS_PROJECT=your-project-name

# Single team (optional)
# AZURE_DEVOPS_TEAM=your-team-name

# OR multiple teams (comma-separated)
# AZURE_DEVOPS_TEAMS=Frontend,Backend,Mobile

# Jira API Token
# Get from: https://id.atlassian.com/manage-profile/security/api-tokens
JIRA_API_TOKEN=your-jira-api-token
JIRA_EMAIL=your-email@example.com
JIRA_DOMAIN=your-domain.atlassian.net

# Single project (optional, for backward compatibility)
# JIRA_PROJECT=your-project-key

# OR multiple projects (comma-separated)
# JIRA_PROJECTS=PROJ1,PROJ2,PROJ3
`;
        fs.writeFileSync(examplePath, exampleContent, 'utf-8');
        console.log('✅ Created .env.example (safe to commit)');
    }
}
// Export singleton instance
export const credentialsManager = CredentialsManager.getInstance();
//# sourceMappingURL=credentials-manager.js.map