import { instrumentedFetch } from '../utils/fetch-with-timeout.js';
import { summarizeError } from '../utils/error-display.js';
import { logger } from '../utils/logger.js';
const DEFAULT_HOME_ASSISTANT_TIMEOUT_MS = 15_000;
const DEFAULT_HOME_ASSISTANT_RESPONSE_BYTES = 2_000_000;
export class HomeAssistantIntegration {
    baseUrl;
    accessToken;
    timeoutMs;
    maxResponseBytes;
    constructor(options) {
        this.baseUrl = normalizeHomeAssistantBaseUrl(options.baseUrl);
        this.accessToken = options.accessToken?.trim() || undefined;
        this.timeoutMs = options.timeoutMs ?? DEFAULT_HOME_ASSISTANT_TIMEOUT_MS;
        this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_HOME_ASSISTANT_RESPONSE_BYTES;
    }
    async getApiStatus() {
        return this.requestJson('/api/', { auth: Boolean(this.accessToken) });
    }
    async getConfig() {
        return this.requestJson('/api/config', { auth: true });
    }
    async listStates() {
        const payload = await this.requestJson('/api/states', { auth: true });
        return Array.isArray(payload) ? payload.filter(isHomeAssistantState) : [];
    }
    async getState(entityId) {
        const payload = await this.requestJson(`/api/states/${encodeURIComponent(entityId)}`, {
            auth: true,
            notFoundAsNull: true,
        });
        return isHomeAssistantState(payload) ? payload : null;
    }
    async listServices() {
        const payload = await this.requestJson('/api/services', { auth: true });
        return Array.isArray(payload) ? payload.filter(isHomeAssistantService) : [];
    }
    async callService(input) {
        const path = `/api/services/${encodeURIComponent(input.domain)}/${encodeURIComponent(input.service)}`
            + (input.returnResponse ? '?return_response' : '');
        return this.requestJson(path, {
            auth: true,
            method: 'POST',
            body: input.serviceData ?? {},
        });
    }
    async fireEvent(eventType, eventData = {}) {
        return this.requestJson(`/api/events/${encodeURIComponent(eventType)}`, {
            auth: true,
            method: 'POST',
            body: eventData,
        });
    }
    async renderTemplate(template, variables) {
        const payload = await this.requestText('/api/template', {
            auth: true,
            method: 'POST',
            body: {
                template,
                ...(variables && Object.keys(variables).length > 0 ? { variables } : {}),
            },
        });
        return payload;
    }
    async publishGoodVibesEvent(eventType, event) {
        return this.fireEvent(eventType, {
            ...event,
            source: 'goodvibes',
            emittedAt: new Date().toISOString(),
        });
    }
    async requestJson(path, options = {}) {
        const response = await this.request(path, options);
        if (options.notFoundAsNull && response.status === 404)
            return null;
        const text = await readResponseTextWithinLimit(response, this.maxResponseBytes);
        if (!response.ok) {
            throw new Error(`Home Assistant HTTP ${response.status}${text ? `: ${text.slice(0, 500)}` : ''}`);
        }
        if (!text.trim())
            return null;
        try {
            return JSON.parse(text);
        }
        catch {
            return text;
        }
    }
    async requestText(path, options = {}) {
        const response = await this.request(path, options);
        const text = await readResponseTextWithinLimit(response, this.maxResponseBytes);
        if (!response.ok) {
            throw new Error(`Home Assistant HTTP ${response.status}${text ? `: ${text.slice(0, 500)}` : ''}`);
        }
        return text;
    }
    async request(path, options) {
        if (options.auth && !this.accessToken) {
            throw new Error('Home Assistant access token is not configured.');
        }
        const headers = {
            Accept: 'application/json',
            ...(options.body ? { 'Content-Type': 'application/json' } : {}),
            ...(options.auth && this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {}),
        };
        if (/^[a-z][a-z0-9+\-.]*:/i.test(path)) {
            throw new Error(`Absolute path not allowed in Home Assistant request: ${path}`);
        }
        return instrumentedFetch(new URL(path, `${this.baseUrl}/`).toString(), {
            method: options.method ?? 'GET',
            headers,
            ...(options.body ? { body: JSON.stringify(options.body) } : {}),
            signal: AbortSignal.timeout(this.timeoutMs),
        });
    }
}
export function normalizeHomeAssistantBaseUrl(value) {
    const trimmed = value.trim();
    if (!trimmed)
        throw new Error('Home Assistant base URL is required.');
    let url;
    try {
        url = new URL(trimmed);
    }
    catch {
        throw new Error(`Invalid Home Assistant base URL: ${trimmed}`);
    }
    if (url.protocol !== 'http:' && url.protocol !== 'https:') {
        throw new Error('Home Assistant base URL must use http or https.');
    }
    url.pathname = url.pathname.replace(/\/+$/, '');
    url.search = '';
    url.hash = '';
    return url.toString().replace(/\/+$/, '');
}
function isHomeAssistantState(value) {
    if (!value || typeof value !== 'object')
        return false;
    const record = value;
    return typeof record.entity_id === 'string' && typeof record.state === 'string';
}
function isHomeAssistantService(value) {
    if (!value || typeof value !== 'object')
        return false;
    const record = value;
    return typeof record.domain === 'string'
        && (Array.isArray(record.services) || (typeof record.services === 'object' && record.services !== null));
}
async function readResponseTextWithinLimit(response, maxBytes) {
    const contentLength = Number.parseInt(response.headers.get('content-length') ?? '0', 10);
    if (Number.isFinite(contentLength) && contentLength > maxBytes) {
        if (response.body)
            await cancelHomeAssistantBody(response.body, 'Response content-length exceeds limit');
        throw new Error(`Home Assistant response too large: ${contentLength} bytes`);
    }
    if (!response.body)
        return '';
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let total = 0;
    let text = '';
    try {
        for (;;) {
            const { done, value } = await reader.read();
            if (done)
                break;
            total += value.byteLength;
            if (total > maxBytes) {
                await cancelHomeAssistantReader(reader, 'Response too large');
                throw new Error(`Home Assistant response exceeded ${maxBytes} bytes`);
            }
            text += decoder.decode(value, { stream: true });
        }
        text += decoder.decode();
        return text;
    }
    finally {
        reader.releaseLock();
    }
}
async function cancelHomeAssistantBody(body, reason) {
    try {
        await body.cancel(reason);
    }
    catch (error) {
        logger.warn('Home Assistant response body cancel failed', { reason, error: summarizeError(error) });
    }
}
async function cancelHomeAssistantReader(reader, reason) {
    try {
        await reader.cancel(reason);
    }
    catch (error) {
        logger.warn('Home Assistant response reader cancel failed', { reason, error: summarizeError(error) });
    }
}
