import { endpointSlug } from './slug.js';
import { DomainError } from './tags.js';
const ALLOWED_RELATIONS = new Set([
    'request',
    'response',
    'error',
]);
const ALLOWED_METHODS = new Set([
    'GET',
    'POST',
    'PUT',
    'PATCH',
    'DELETE',
]);
export class EndpointService {
    db;
    tags;
    versions;
    constructor(db, tags, versions) {
        this.db = db;
        this.tags = tags;
        this.versions = versions;
    }
    create(input, actor) {
        const method = this.requireMethod(input.method);
        if (!input.path)
            throw new DomainError('VALIDATION', 'path is required');
        const slug = endpointSlug(method, input.path);
        if (!slug)
            throw new DomainError('VALIDATION', 'slug resolves to empty');
        const tx = this.db.transaction(() => {
            const conflict = this.db.prepare(`SELECT 1 FROM endpoint WHERE slug = ?`).get(slug);
            if (conflict)
                throw new DomainError('SLUG_CONFLICT', `endpoint slug '${slug}' already exists`);
            const info = this.db
                .prepare(`INSERT INTO endpoint (slug, method, path, summary, description)
           VALUES (?, ?, ?, ?, ?)`)
                .run(slug, method, input.path, input.summary ?? '', input.description ?? null);
            const id = Number(info.lastInsertRowid);
            if (input.tags?.length)
                this.tags.assignTags('endpoint', id, input.tags);
            const created = this.getByIdInternal(id);
            this.versions.createVersion('endpoint', id, created, actor, 'Created');
            return created;
        });
        return tx();
    }
    list(query = {}) {
        const where = [];
        const params = [];
        if (query.search) {
            where.push(`(path LIKE ? OR summary LIKE ? OR slug LIKE ?)`);
            const like = `%${query.search}%`;
            params.push(like, like, like);
        }
        let tagFilter = null;
        const tagSlugs = query.tags?.filter(Boolean) ?? [];
        if (tagSlugs.length) {
            const placeholders = tagSlugs.map(() => '?').join(',');
            if (query.tagFilter === 'and') {
                tagFilter = `
          id IN (
            SELECT et.entity_id
              FROM entity_tag et
              JOIN tag t ON t.id = et.tag_id
             WHERE et.entity_type = 'endpoint' AND t.slug IN (${placeholders})
          GROUP BY et.entity_id
            HAVING COUNT(DISTINCT t.slug) = ?
          )
        `;
                params.push(...tagSlugs, tagSlugs.length);
            }
            else {
                tagFilter = `
          id IN (
            SELECT et.entity_id FROM entity_tag et
              JOIN tag t ON t.id = et.tag_id
             WHERE et.entity_type = 'endpoint' AND t.slug IN (${placeholders})
          )
        `;
                params.push(...tagSlugs);
            }
        }
        if (tagFilter)
            where.push(tagFilter);
        const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
        const limit = Math.min(Math.max(query.limit ?? 200, 1), 500);
        const offset = Math.max(query.offset ?? 0, 0);
        const rows = this.db
            .prepare(`SELECT * FROM endpoint
         ${whereSql}
         ORDER BY path, method
         LIMIT ? OFFSET ?`)
            .all(...params, limit, offset);
        return rows.map((r) => this.hydrate(r));
    }
    getBySlug(slug) {
        const row = this.db.prepare(`SELECT * FROM endpoint WHERE slug = ?`).get(slug);
        return row ? this.hydrate(row) : null;
    }
    getIdBySlug(slug) {
        const row = this.db.prepare(`SELECT id FROM endpoint WHERE slug = ?`).get(slug);
        return row?.id ?? null;
    }
    update(slug, input, actor) {
        const tx = this.db.transaction(() => {
            const current = this.db.prepare(`SELECT * FROM endpoint WHERE slug = ?`).get(slug);
            if (!current)
                throw new DomainError('NOT_FOUND', `endpoint '${slug}' not found`);
            const method = input.method ? this.requireMethod(input.method) : current.method;
            const path = input.path ?? current.path;
            const autoSlug = endpointSlug(method, path);
            const nextSlug = input.newSlug ?? autoSlug;
            if (nextSlug !== slug) {
                const conflict = this.db.prepare(`SELECT 1 FROM endpoint WHERE slug = ?`).get(nextSlug);
                if (conflict)
                    throw new DomainError('SLUG_CONFLICT', `endpoint slug '${nextSlug}' already exists`);
            }
            const nextRow = {
                ...current,
                slug: nextSlug,
                method,
                path,
                summary: input.summary ?? current.summary,
                description: input.description !== undefined ? input.description : current.description,
            };
            this.db
                .prepare(`UPDATE endpoint
             SET slug = ?, method = ?, path = ?, summary = ?, description = ?,
                 updated_at = datetime('now')
           WHERE id = ?`)
                .run(nextRow.slug, nextRow.method, nextRow.path, nextRow.summary, nextRow.description, current.id);
            if (input.tags)
                this.tags.assignTags('endpoint', current.id, input.tags);
            const updated = this.getByIdInternal(current.id);
            const summary = nextSlug !== slug ? `Renamed from '${slug}' to '${nextSlug}'` : 'Updated';
            this.versions.createVersion('endpoint', current.id, updated, actor, summary);
            return updated;
        });
        return tx();
    }
    remove(slug, actor) {
        const tx = this.db.transaction(() => {
            const row = this.db.prepare(`SELECT * FROM endpoint WHERE slug = ?`).get(slug);
            if (!row)
                throw new DomainError('NOT_FOUND', `endpoint '${slug}' not found`);
            this.versions.createVersion('endpoint', row.id, null, actor, 'Deleted');
            this.db
                .prepare(`DELETE FROM entity_tag WHERE entity_type = 'endpoint' AND entity_id = ?`)
                .run(row.id);
            this.db.prepare(`DELETE FROM endpoint WHERE id = ?`).run(row.id);
            const brokenReferences = [];
            return { deleted: true, brokenReferences };
        });
        return tx();
    }
    getByIdInternal(id) {
        const row = this.db.prepare(`SELECT * FROM endpoint WHERE id = ?`).get(id);
        if (!row)
            throw new Error(`endpoint id ${id} disappeared mid-tx`);
        return this.hydrate(row);
    }
    hydrate(row) {
        return {
            slug: row.slug,
            method: row.method,
            path: row.path,
            summary: row.summary,
            description: row.description,
            tags: this.tags.getEntityTagSlugs('endpoint', row.id),
            dtos: this.getLinkedDtos(row.id),
            createdAt: row.created_at,
            updatedAt: row.updated_at,
        };
    }
    getLinkedDtos(endpointId) {
        const rows = this.db
            .prepare(`SELECT d.slug AS dto_slug, d.name AS dto_name, ed.relation AS relation, ed.status_code AS status_code
           FROM endpoint_dto ed
           JOIN dto d ON d.id = ed.dto_id
          WHERE ed.endpoint_id = ?
          ORDER BY ed.relation, ed.status_code, d.name`)
            .all(endpointId);
        return rows.map((r) => ({
            dtoSlug: r.dto_slug,
            dtoName: r.dto_name,
            relation: r.relation,
            statusCode: r.status_code,
        }));
    }
    linkDto(endpointSlug, dtoSlug, relation, statusCode = null) {
        if (!ALLOWED_RELATIONS.has(relation)) {
            throw new DomainError('VALIDATION', `invalid relation '${relation}'`);
        }
        if (relation === 'request' && statusCode !== null) {
            throw new DomainError('VALIDATION', `request relation must not carry a status code`);
        }
        const ep = this.db.prepare(`SELECT id FROM endpoint WHERE slug = ?`).get(endpointSlug);
        if (!ep)
            throw new DomainError('NOT_FOUND', `endpoint '${endpointSlug}' not found`);
        const dto = this.db.prepare(`SELECT id FROM dto WHERE slug = ?`).get(dtoSlug);
        if (!dto)
            throw new DomainError('NOT_FOUND', `dto '${dtoSlug}' not found`);
        this.db
            .prepare(`INSERT OR IGNORE INTO endpoint_dto (endpoint_id, dto_id, relation, status_code)
           VALUES (?, ?, ?, ?)`)
            .run(ep.id, dto.id, relation, statusCode);
        return this.getByIdInternal(ep.id);
    }
    unlinkDto(endpointSlug, dtoSlug, relation, statusCode = null) {
        const ep = this.db.prepare(`SELECT id FROM endpoint WHERE slug = ?`).get(endpointSlug);
        if (!ep)
            throw new DomainError('NOT_FOUND', `endpoint '${endpointSlug}' not found`);
        const dto = this.db.prepare(`SELECT id FROM dto WHERE slug = ?`).get(dtoSlug);
        if (!dto)
            throw new DomainError('NOT_FOUND', `dto '${dtoSlug}' not found`);
        if (statusCode === null) {
            this.db
                .prepare(`DELETE FROM endpoint_dto
             WHERE endpoint_id = ? AND dto_id = ? AND relation = ? AND status_code IS NULL`)
                .run(ep.id, dto.id, relation);
        }
        else {
            this.db
                .prepare(`DELETE FROM endpoint_dto
             WHERE endpoint_id = ? AND dto_id = ? AND relation = ? AND status_code = ?`)
                .run(ep.id, dto.id, relation, statusCode);
        }
        return this.getByIdInternal(ep.id);
    }
    requireMethod(m) {
        const upper = m.toUpperCase();
        if (!ALLOWED_METHODS.has(upper)) {
            throw new DomainError('VALIDATION', `unsupported method '${m}'`);
        }
        return upper;
    }
}
//# sourceMappingURL=endpoint.js.map