"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var HetznerDnsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HetznerDnsService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const axios_1 = __importDefault(require("axios"));
const https = __importStar(require("node:https"));
const generated_1 = require("../../../modules/providers/implementations/hetzner/generated");
const dns_provider_interface_1 = require("../interfaces/dns-provider.interface");
const cloud_provider_enum_1 = require("../enums/cloud-provider.enum");
/**
 * Hetzner DNS Service - Cloud API implementation
 *
 * Uses the Hetzner Cloud API (api.hetzner.cloud/v1) with the same Bearer token
 * as other Hetzner Cloud services. The Cloud API uses RRSets (grouped by name+type)
 * instead of individual records. This service maps between the IDnsProvider
 * individual record interface and the RRSet model.
 *
 * Record IDs are composed as "{name}/{type}:{value}" to uniquely identify
 * individual values within an RRSet.
 */
let HetznerDnsService = HetznerDnsService_1 = class HetznerDnsService {
    constructor(configService, credentialProvider) {
        this.configService = configService;
        this.credentialProvider = credentialProvider;
        this.logger = new common_1.Logger(HetznerDnsService_1.name);
        this.basePath = this.configService.get('HETZNER_API_BASE_PATH', 'https://api.hetzner.cloud/v1');
        this.timeout = Number.parseInt(this.configService.get('HETZNER_TIMEOUT', '10000'), 10);
    }
    createAxiosInstance() {
        const httpsAgent = new https.Agent({
            keepAlive: false,
            rejectUnauthorized: true,
            family: 4,
        });
        const instance = axios_1.default.create({
            timeout: this.timeout,
            httpsAgent,
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                'User-Agent': 'Flui-Cloud-API/1.0',
            },
            validateStatus: (status) => status >= 200 && status < 300,
        });
        instance.interceptors.request.use((config) => {
            this.logger.debug(`Hetzner DNS API Request: ${config.method?.toUpperCase()} ${config.url}`);
            return config;
        }, (error) => {
            this.logger.error(`Hetzner DNS API Request Error: ${error.message}`);
            return Promise.reject(error);
        });
        instance.interceptors.response.use((response) => {
            this.logger.debug(`Hetzner DNS API Response: ${response.status} ${response.statusText}`);
            return response;
        }, (error) => {
            if (error.response) {
                this.logger.error(`Hetzner DNS API Error: ${error.response.status} ${error.response.statusText}`);
                this.logger.error(`Error Data: ${JSON.stringify(error.response.data, null, 2)}`);
            }
            else if (error.request) {
                this.logger.error(`Hetzner DNS API No Response Received - code: ${error.code}, message: ${error.message}`);
            }
            else {
                this.logger.error(`Hetzner DNS API Error: ${error.message}`);
            }
            return Promise.reject(error);
        });
        return instance;
    }
    async createConfiguration() {
        const token = await this.credentialProvider.getActiveApiToken(cloud_provider_enum_1.CloudProvider.HETZNER);
        return new generated_1.Configuration({
            accessToken: token,
            basePath: this.basePath,
            baseOptions: {
                headers: {
                    'User-Agent': 'Flui-Cloud-API/1.0',
                    'Content-Type': 'application/json',
                    Accept: 'application/json',
                },
            },
        });
    }
    async createZonesApi() {
        const token = await this.credentialProvider.getActiveApiToken(cloud_provider_enum_1.CloudProvider.HETZNER);
        const configuration = await this.createConfiguration();
        const axiosInstance = this.createAxiosInstance();
        axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
        return new generated_1.ZonesApi(configuration, this.basePath, axiosInstance);
    }
    async createRRSetsApi() {
        const token = await this.credentialProvider.getActiveApiToken(cloud_provider_enum_1.CloudProvider.HETZNER);
        const configuration = await this.createConfiguration();
        const axiosInstance = this.createAxiosInstance();
        axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
        return new generated_1.ZoneRRSetsApi(configuration, this.basePath, axiosInstance);
    }
    // ── Zone operations ──────────────────────────────────────────────
    async listZones() {
        const zonesApi = await this.createZonesApi();
        const response = await zonesApi.listZones();
        return response.data.zones.map((zone) => this.mapZone(zone));
    }
    async getZone(zoneId) {
        try {
            const zonesApi = await this.createZonesApi();
            const response = await zonesApi.getZone(zoneId);
            return this.mapZone(response.data.zone);
        }
        catch (error) {
            if (error.response?.status === 404) {
                return null;
            }
            throw error;
        }
    }
    async getZoneByName(name) {
        const zonesApi = await this.createZonesApi();
        const response = await zonesApi.listZones(name);
        const zone = response.data.zones.find((z) => z.name === name);
        return zone ? this.mapZone(zone) : null;
    }
    // ── Record operations (mapped to RRSets) ────────────────────────
    async listRecords(zoneId) {
        const rrSetsApi = await this.createRRSetsApi();
        const allRecords = [];
        let page = 1;
        const perPage = 100;
        // Paginate through all RRSets
        while (true) {
            const response = await rrSetsApi.listZoneRrsets(zoneId, undefined, // name filter
            undefined, // type filter
            undefined, // label selector
            undefined, // sort
            page, perPage);
            for (const rrset of response.data.rrsets) {
                const mappedRecords = this.flattenRRSet(rrset, zoneId);
                allRecords.push(...mappedRecords);
            }
            // Check if we got fewer results than page size (last page)
            if (response.data.rrsets.length < perPage) {
                break;
            }
            page++;
        }
        return allRecords;
    }
    async getRecord(zoneId, recordId) {
        const parsed = this.parseRecordId(recordId);
        if (!parsed) {
            return null;
        }
        try {
            const rrSetsApi = await this.createRRSetsApi();
            const response = await rrSetsApi.getZoneRrset(zoneId, parsed.name, parsed.type);
            const rrset = response.data.rrset;
            const matchingRecord = rrset.records.find((r) => r.value === parsed.value);
            if (!matchingRecord) {
                return null;
            }
            return {
                recordId,
                zoneId: zoneId,
                type: rrset.type,
                name: rrset.name,
                value: matchingRecord.value,
                ttl: rrset.ttl ?? 0,
            };
        }
        catch (error) {
            if (error.response?.status === 404) {
                return null;
            }
            throw error;
        }
    }
    async createRecord(config) {
        const rrSetsApi = await this.createRRSetsApi();
        const ttl = config.ttl ?? 300;
        // Check if an RRSet already exists for this name+type
        let existingRRSet = null;
        try {
            const response = await rrSetsApi.getZoneRrset(config.zoneId, config.name, config.type);
            existingRRSet = response.data.rrset;
        }
        catch (error) {
            if (error.response?.status !== 404) {
                throw error;
            }
        }
        if (existingRRSet) {
            // RRSet exists — add the new value by replacing all records with existing + new
            const existingValues = existingRRSet.records.map((r) => r.value);
            if (existingValues.includes(config.value)) {
                // Value already exists, return it as-is
                this.logger.debug(`DNS record already exists: ${config.type} ${config.name} -> ${config.value}`);
                return {
                    recordId: this.composeRecordId(config.name, config.type, config.value),
                    zoneId: config.zoneId,
                    type: config.type,
                    name: config.name,
                    value: config.value,
                    ttl: existingRRSet.ttl ?? ttl,
                };
            }
            // Add the new record value to the existing RRSet using set-records
            const newRecords = [
                ...existingRRSet.records,
                { value: config.value },
            ];
            await this.setRRSetRecords(rrSetsApi, config.zoneId, config.name, config.type, newRecords);
        }
        else {
            // Create new RRSet
            const createRequest = {
                name: config.name,
                type: config.type,
                ttl,
                records: [{ value: config.value }],
            };
            await rrSetsApi.createZoneRrset(config.zoneId, createRequest);
        }
        if (config.labels && Object.keys(config.labels).length > 0) {
            await this.applyLabels(rrSetsApi, config.zoneId, config.name, config.type, config.labels);
        }
        this.logger.log(`Created DNS record: ${config.type} ${config.name} -> ${config.value}`);
        return {
            recordId: this.composeRecordId(config.name, config.type, config.value),
            zoneId: config.zoneId,
            type: config.type,
            name: config.name,
            value: config.value,
            ttl,
            labels: config.labels,
        };
    }
    async updateRecord(config) {
        const parsed = this.parseRecordId(config.recordId);
        if (!parsed) {
            throw new Error(`Invalid record ID format: ${config.recordId}`);
        }
        const rrSetsApi = await this.createRRSetsApi();
        const ttl = config.ttl ?? 300;
        // If name+type changed, we need to delete from old RRSet and create in new one
        if (parsed.name !== config.name || parsed.type !== config.type) {
            await this.removeValueFromRRSet(rrSetsApi, config.zoneId, parsed.name, parsed.type, parsed.value);
            return this.createRecord({
                zoneId: config.zoneId,
                type: config.type,
                name: config.name,
                value: config.value,
                ttl,
            });
        }
        // Same name+type — replace old value with new value in the RRSet
        try {
            const response = await rrSetsApi.getZoneRrset(config.zoneId, config.name, config.type);
            const rrset = response.data.rrset;
            const newRecords = rrset.records
                .filter((r) => r.value !== parsed.value)
                .concat([{ value: config.value }]);
            await this.setRRSetRecords(rrSetsApi, config.zoneId, config.name, config.type, newRecords);
        }
        catch (error) {
            if (error.response?.status === 404) {
                // RRSet doesn't exist, create it
                return this.createRecord({
                    zoneId: config.zoneId,
                    type: config.type,
                    name: config.name,
                    value: config.value,
                    ttl,
                });
            }
            throw error;
        }
        this.logger.log(`Updated DNS record ${config.recordId}: ${config.type} ${config.name} -> ${config.value}`);
        return {
            recordId: this.composeRecordId(config.name, config.type, config.value),
            zoneId: config.zoneId,
            type: config.type,
            name: config.name,
            value: config.value,
            ttl,
        };
    }
    async deleteRecord(zoneId, recordId) {
        const parsed = this.parseRecordId(recordId);
        if (!parsed) {
            throw new Error(`Invalid record ID format: ${recordId}`);
        }
        const rrSetsApi = await this.createRRSetsApi();
        await this.removeValueFromRRSet(rrSetsApi, zoneId, parsed.name, parsed.type, parsed.value);
        this.logger.log(`Deleted DNS record ${recordId} from zone ${zoneId}`);
    }
    async bulkCreateRecords(zoneId, records) {
        // Group records by name+type to batch into RRSet operations
        const grouped = new Map();
        for (const record of records) {
            const key = `${record.name}/${record.type}`;
            const existing = grouped.get(key) || [];
            existing.push(record);
            grouped.set(key, existing);
        }
        const rrSetsApi = await this.createRRSetsApi();
        const results = [];
        for (const [, groupRecords] of grouped) {
            const first = groupRecords[0];
            const ttl = first.ttl ?? 300;
            // Check if RRSet already exists
            let existingRRSet = null;
            try {
                const response = await rrSetsApi.getZoneRrset(zoneId, first.name, first.type);
                existingRRSet = response.data.rrset;
            }
            catch (error) {
                if (error.response?.status !== 404) {
                    throw error;
                }
            }
            const newValues = groupRecords.map((r) => r.value);
            if (existingRRSet) {
                // Merge with existing records, avoiding duplicates
                const existingValues = new Set(existingRRSet.records.map((r) => r.value));
                const allRecords = [...existingRRSet.records];
                for (const value of newValues) {
                    if (!existingValues.has(value)) {
                        allRecords.push({ value });
                    }
                }
                await this.setRRSetRecords(rrSetsApi, zoneId, first.name, first.type, allRecords);
            }
            else {
                // Create new RRSet with all values
                const createRequest = {
                    name: first.name,
                    type: first.type,
                    ttl,
                    records: newValues.map((v) => ({ value: v })),
                };
                await rrSetsApi.createZoneRrset(zoneId, createRequest);
            }
            for (const record of groupRecords) {
                results.push({
                    recordId: this.composeRecordId(record.name, record.type, record.value),
                    zoneId,
                    type: record.type,
                    name: record.name,
                    value: record.value,
                    ttl,
                });
            }
        }
        this.logger.log(`Bulk created ${results.length} DNS records in zone ${zoneId}`);
        return results;
    }
    async purgeARecords(zoneId) {
        const rrSetsApi = await this.createRRSetsApi();
        const records = await this.listRecords(zoneId);
        const aNames = [
            ...new Set(records.filter((r) => r.type === dns_provider_interface_1.DnsRecordType.A).map((r) => r.name)),
        ];
        let deleted = 0;
        for (const name of aNames) {
            try {
                await rrSetsApi.deleteZoneRrset(zoneId, name, generated_1.DeleteZoneRrsetRrTypeEnum.A);
                deleted++;
            }
            catch (error) {
                if (error.response?.status === 404)
                    continue;
                throw error;
            }
        }
        return deleted;
    }
    async testConnection() {
        try {
            await this.listZones();
            return { success: true };
        }
        catch (error) {
            return {
                success: false,
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    // ── RRSet helpers ────────────────────────────────────────────────
    /**
     * Replace all records in an RRSet using the set-records endpoint.
     * The Cloud API uses POST /zones/{id}/rrsets/{name}/{type}/records
     * with body { records: [...] } to replace all record values.
     *
     * Since the generated client's updateZoneRrset only supports label changes,
     * we use a direct axios call for the set-records operation.
     */
    async setRRSetRecords(rrSetsApi, zoneId, name, type, records) {
        // The generated client doesn't expose set-records directly,
        // so we delete the RRSet and recreate it with the new values
        try {
            await rrSetsApi.deleteZoneRrset(zoneId, name, type);
        }
        catch (error) {
            // If the RRSet doesn't exist, that's fine
            if (error.response?.status !== 404) {
                throw error;
            }
        }
        // Only recreate if there are records to add
        if (records.length > 0) {
            const createRequest = {
                name,
                type: type,
                records: records.map((r) => ({ value: r.value })),
            };
            await rrSetsApi.createZoneRrset(zoneId, createRequest);
        }
    }
    async applyLabels(rrSetsApi, zoneId, name, type, labels) {
        try {
            await rrSetsApi.updateZoneRrset(zoneId, name, type, { labels });
        }
        catch (error) {
            this.logger.warn(`Failed to apply labels to RRSet ${name}/${type}: ${error.message}`);
        }
    }
    /**
     * Remove a single value from an RRSet.
     * If it's the last value, deletes the entire RRSet.
     */
    async removeValueFromRRSet(rrSetsApi, zoneId, name, type, value) {
        try {
            const response = await rrSetsApi.getZoneRrset(zoneId, name, type);
            const rrset = response.data.rrset;
            const remaining = rrset.records.filter((r) => r.value !== value);
            if (remaining.length === 0) {
                // Delete the entire RRSet
                await rrSetsApi.deleteZoneRrset(zoneId, name, type);
            }
            else {
                // Replace with remaining records
                await this.setRRSetRecords(rrSetsApi, zoneId, name, type, remaining);
            }
        }
        catch (error) {
            if (error.response?.status === 404) {
                // Already gone — nothing to do
                return;
            }
            throw error;
        }
    }
    // ── Mapping helpers ──────────────────────────────────────────────
    /**
     * Compose a synthetic record ID from name, type, and value.
     * Format: "{name}/{type}:{value}"
     */
    composeRecordId(name, type, value) {
        return `${name}/${type}:${value}`;
    }
    /**
     * Parse a synthetic record ID back into its components.
     * Expected format: "{name}/{type}:{value}"
     */
    parseRecordId(recordId) {
        // Format: name/TYPE:value
        const slashIndex = recordId.indexOf('/');
        if (slashIndex === -1) {
            return null;
        }
        const name = recordId.substring(0, slashIndex);
        const rest = recordId.substring(slashIndex + 1);
        const colonIndex = rest.indexOf(':');
        if (colonIndex === -1) {
            return null;
        }
        const type = rest.substring(0, colonIndex);
        const value = rest.substring(colonIndex + 1);
        if (!name || !type || !value) {
            return null;
        }
        return { name, type, value };
    }
    /**
     * Flatten an RRSet into individual DnsRecordInfo entries.
     * Each record value becomes a separate entry with a composed recordId.
     */
    flattenRRSet(rrset, zoneId) {
        return rrset.records.map((record) => ({
            recordId: this.composeRecordId(rrset.name, rrset.type, record.value),
            zoneId,
            type: rrset.type,
            name: rrset.name,
            value: record.value,
            ttl: rrset.ttl ?? 0,
            labels: rrset.labels,
        }));
    }
    mapZone(zone) {
        return {
            zoneId: zone.id.toString(),
            name: zone.name,
            ttl: zone.ttl,
            recordCount: zone.record_count,
            status: zone.status,
        };
    }
};
exports.HetznerDnsService = HetznerDnsService;
exports.HetznerDnsService = HetznerDnsService = HetznerDnsService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(1, (0, common_1.Inject)('ICredentialProvider')),
    __metadata("design:paramtypes", [config_1.ConfigService, Object])
], HetznerDnsService);
