import { uiViewSlug } from './slug.js';
import { DomainError } from './tags.js';
const VALID_LOCATIONS = ['path', 'query', 'hash'];
export class UiViewService {
    db;
    tags;
    versions;
    constructor(db, tags, versions) {
        this.db = db;
        this.tags = tags;
        this.versions = versions;
    }
    create(input, actor) {
        if (!input.name)
            throw new DomainError('VALIDATION', 'name is required');
        const slug = input.slug?.trim() || uiViewSlug(input.name);
        if (!slug)
            throw new DomainError('VALIDATION', 'slug resolves to empty');
        const params = input.params ?? [];
        const url = normaliseUrl(input.url);
        const tx = this.db.transaction(() => {
            const conflict = this.db.prepare(`SELECT 1 FROM ui_view WHERE slug = ?`).get(slug);
            if (conflict)
                throw new DomainError('SLUG_CONFLICT', `ui view slug '${slug}' already exists`);
            const info = this.db
                .prepare(`INSERT INTO ui_view (slug, name, url, description, params)
           VALUES (?, ?, ?, ?, ?)`)
                .run(slug, input.name, url, input.description ?? null, JSON.stringify(params));
            const id = Number(info.lastInsertRowid);
            if (input.tags?.length)
                this.tags.assignTags('ui-view', id, input.tags);
            const created = this.getByIdInternal(id);
            this.versions.createVersion('ui-view', id, created, actor, 'Created');
            const warnings = computeWarnings(url, params);
            return { uiView: created, warnings };
        });
        return tx();
    }
    list(query = {}) {
        const where = [];
        const params = [];
        if (query.search) {
            where.push(`(name LIKE ? OR description LIKE ? OR slug LIKE ? OR url LIKE ?)`);
            const like = `%${query.search}%`;
            params.push(like, like, like, like);
        }
        const tagSlugs = query.tags?.filter(Boolean) ?? [];
        if (tagSlugs.length) {
            const placeholders = tagSlugs.map(() => '?').join(',');
            if (query.tagFilter === 'or') {
                where.push(`
          id IN (
            SELECT et.entity_id FROM entity_tag et
              JOIN tag t ON t.id = et.tag_id
             WHERE et.entity_type = 'ui-view' AND t.slug IN (${placeholders})
          )
        `);
                params.push(...tagSlugs);
            }
            else {
                where.push(`
          id IN (
            SELECT et.entity_id
              FROM entity_tag et
              JOIN tag t ON t.id = et.tag_id
             WHERE et.entity_type = 'ui-view' AND t.slug IN (${placeholders})
          GROUP BY et.entity_id
            HAVING COUNT(DISTINCT t.slug) = ?
          )
        `);
                params.push(...tagSlugs, tagSlugs.length);
            }
        }
        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 ui_view ${whereSql}
         ORDER BY name
         LIMIT ? OFFSET ?`)
            .all(...params, limit, offset);
        return rows.map((r) => this.hydrate(r));
    }
    getBySlug(slug) {
        const row = this.db.prepare(`SELECT * FROM ui_view WHERE slug = ?`).get(slug);
        return row ? this.hydrate(row) : null;
    }
    getIdBySlug(slug) {
        const row = this.db.prepare(`SELECT id FROM ui_view WHERE slug = ?`).get(slug);
        return row?.id ?? null;
    }
    update(slug, input, actor) {
        const tx = this.db.transaction(() => {
            const current = this.db.prepare(`SELECT * FROM ui_view WHERE slug = ?`).get(slug);
            if (!current)
                throw new DomainError('NOT_FOUND', `ui view '${slug}' not found`);
            const nextName = input.name ?? current.name;
            const autoSlug = uiViewSlug(nextName);
            const nextSlug = input.newSlug?.trim() || autoSlug;
            if (nextSlug !== slug) {
                const conflict = this.db.prepare(`SELECT 1 FROM ui_view WHERE slug = ?`).get(nextSlug);
                if (conflict)
                    throw new DomainError('SLUG_CONFLICT', `ui view slug '${nextSlug}' already exists`);
            }
            const nextParams = input.params !== undefined ? JSON.stringify(input.params) : current.params;
            const nextUrl = input.url !== undefined ? normaliseUrl(input.url) : current.url;
            this.db
                .prepare(`UPDATE ui_view
             SET slug = ?, name = ?, url = ?, description = ?, params = ?,
                 updated_at = datetime('now')
           WHERE id = ?`)
                .run(nextSlug, nextName, nextUrl, input.description !== undefined ? input.description : current.description, nextParams, current.id);
            if (input.tags)
                this.tags.assignTags('ui-view', current.id, input.tags);
            const updated = this.getByIdInternal(current.id);
            const summary = nextSlug !== slug ? `Renamed from '${slug}' to '${nextSlug}'` : 'Updated';
            this.versions.createVersion('ui-view', current.id, updated, actor, summary);
            const warnings = computeWarnings(updated.url, updated.params);
            return { uiView: updated, previousSlug: slug, warnings };
        });
        return tx();
    }
    remove(slug, actor, brokenReferences = []) {
        const tx = this.db.transaction(() => {
            const row = this.db.prepare(`SELECT * FROM ui_view WHERE slug = ?`).get(slug);
            if (!row)
                throw new DomainError('NOT_FOUND', `ui view '${slug}' not found`);
            this.versions.createVersion('ui-view', row.id, null, actor, 'Deleted');
            this.db
                .prepare(`DELETE FROM entity_tag WHERE entity_type = 'ui-view' AND entity_id = ?`)
                .run(row.id);
            this.db.prepare(`DELETE FROM ui_view WHERE id = ?`).run(row.id);
            return { deleted: true, brokenReferences };
        });
        return tx();
    }
    getByIdInternal(id) {
        const row = this.db.prepare(`SELECT * FROM ui_view WHERE id = ?`).get(id);
        if (!row)
            throw new Error(`ui_view id ${id} disappeared mid-tx`);
        return this.hydrate(row);
    }
    hydrate(row) {
        return {
            slug: row.slug,
            name: row.name,
            url: row.url,
            description: row.description,
            params: parseParams(row.params),
            tags: this.tags.getEntityTagSlugs('ui-view', row.id),
            createdAt: row.created_at,
            updatedAt: row.updated_at,
        };
    }
}
function normaliseUrl(value) {
    if (value === undefined || value === null)
        return null;
    const trimmed = value.trim();
    return trimmed === '' ? null : trimmed;
}
function parseParams(raw) {
    try {
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed))
            return [];
        return parsed
            .filter((p) => p && typeof p === 'object' && typeof p.name === 'string')
            .map((p) => {
            const inValue = String(p.in ?? '');
            const location = VALID_LOCATIONS.includes(inValue)
                ? inValue
                : 'query';
            const out = {
                name: String(p.name),
                in: location,
            };
            if (typeof p.type === 'string')
                out.type = p.type;
            if (typeof p.required === 'boolean')
                out.required = p.required;
            if (typeof p.default === 'string')
                out.default = p.default;
            if (typeof p.description === 'string')
                out.description = p.description;
            return out;
        });
    }
    catch {
        return [];
    }
}
const PATH_PARAM_RE = /:([a-zA-Z_][a-zA-Z0-9_]*)/g;
export function computeWarnings(url, params) {
    const warnings = [];
    for (let i = 0; i < params.length; i++) {
        const p = params[i];
        if (!p.name) {
            warnings.push(`params[${i}]: missing 'name'`);
        }
        if (!VALID_LOCATIONS.includes(p.in)) {
            warnings.push(`params[${i}] '${p.name ?? '?'}': invalid 'in' value '${p.in}' (expected path|query|hash)`);
        }
    }
    const seen = new Set();
    for (const p of params) {
        const key = `${p.in}::${p.name}`;
        if (seen.has(key)) {
            warnings.push(`Duplicate param (name='${p.name}', in='${p.in}')`);
        }
        seen.add(key);
    }
    const urlPathParams = new Set();
    if (url) {
        const matches = url.matchAll(PATH_PARAM_RE);
        for (const m of matches)
            urlPathParams.add(m[1]);
    }
    const declaredPathParams = new Set(params.filter((p) => p.in === 'path' && p.name).map((p) => p.name));
    for (const name of urlPathParams) {
        if (!declaredPathParams.has(name)) {
            warnings.push(`path param ":${name}" in URL not declared in params[]`);
        }
    }
    for (const name of declaredPathParams) {
        if (url && !urlPathParams.has(name)) {
            warnings.push(`path param '${name}' declared but not present in URL`);
        }
    }
    if (url === null && declaredPathParams.size > 0) {
        warnings.push(`path params declared but URL is null (modal/drawer should not have path params)`);
    }
    return warnings;
}
//# sourceMappingURL=ui-view.js.map