import { createAdapter, createMcpServer, extractText, mcpTool } from '@inharness-ai/agent-adapters';
import { z } from 'zod';
import { DomainError } from '../services/tags.js';
import { pluginHost } from '../core/plugin-host/host.js';
import { RawEntityReader, isRawEntityType } from '../domain/raw-entity-reader.js';
import { parseXmlTagsExcludingCode, taggedListVia } from '../../shared/xml-tags.js';
import { listExtensionReferenceTypes } from '../../shared/reference-extensions.js';
import { readConfig } from '../config.js';
export function createReferenceToolsServer(deps) {
    const ok = (payload) => ({
        content: [{ type: 'text', text: JSON.stringify(payload) }],
    });
    const fail = (err) => {
        const code = err instanceof DomainError ? err.code : 'INTERNAL';
        const message = err instanceof Error ? err.message : String(err);
        return {
            content: [{ type: 'text', text: JSON.stringify({ error: message, code }) }],
            isError: true,
        };
    };
    // entityType is open-ended now — runtime validation against the host's
    // available manifests, not a literal-union schema. Keeps the tool schema
    // stable when plugins are added/removed.
    const entityTypeSchema = z.string();
    const validateActiveType = (type) => {
        if (!pluginHost.getEntity(type)) {
            const known = pluginHost.listAvailable().map((m) => m.type);
            throw new DomainError('VALIDATION', `unsupported or inactive entity type '${type}'. Active: [${known.join(', ')}]`);
        }
        // Cast: EntityType is a literal union pinned to the four core plugins.
        // Runtime validation is via pluginHost.getEntity(); the narrowing exists
        // only to satisfy TagsService / ReferencesService signatures until
        // EntityType is relaxed to `string` (Phase 4 follow-up).
        return type;
    };
    const createTag = mcpTool('create_tag', 'Create a new tag for classifying entities.', {
        name: z.string().describe('Display name'),
        color: z.string().optional().describe('Hex color (e.g. #4A90D9)'),
        description: z.string().optional(),
    }, async (args) => {
        try {
            const tag = deps.tagsService.create({
                name: String(args.name),
                color: args.color,
                description: args.description,
            });
            deps.ws.broadcast({ kind: 'tag:changed', slug: tag.slug });
            return ok({ slug: tag.slug, name: tag.name });
        }
        catch (err) {
            return fail(err);
        }
    });
    const updateTag = mcpTool('update_tag', 'Update tag properties (name, color, description). If name changes, slug is regenerated and references in pages are updated.', {
        slug: z.string(),
        data: z.object({
            name: z.string().optional(),
            color: z.string().nullable().optional(),
            description: z.string().nullable().optional(),
        }),
    }, async (args) => {
        try {
            const data = args.data;
            const oldSlug = String(args.slug);
            const tag = deps.tagsService.update(oldSlug, {
                name: data.name,
                color: data.color,
                description: data.description,
            });
            if (tag.slug !== oldSlug) {
                await deps.referencesService.propagateTagSlugChange(oldSlug, tag.slug);
            }
            deps.ws.broadcast({ kind: 'tag:changed', slug: tag.slug });
            return ok({ slug: tag.slug, updated: true });
        }
        catch (err) {
            return fail(err);
        }
    });
    const deleteTag = mcpTool('delete_tag', 'Delete a tag. Removes all entity-tag assignments (CASCADE).', { slug: z.string() }, async (args) => {
        try {
            const result = deps.tagsService.remove(String(args.slug));
            deps.ws.broadcast({ kind: 'tag:changed', slug: String(args.slug) });
            return ok(result);
        }
        catch (err) {
            return fail(err);
        }
    });
    const listTags = mcpTool('list_tags', 'List all tags with usage counts (per active plugin entity type).', {}, async () => {
        try {
            const tags = deps.tagsService.list();
            return ok({
                tags: tags.map((t) => ({
                    slug: t.slug,
                    name: t.name,
                    color: t.color,
                    description: t.description,
                    counts: t.counts,
                })),
            });
        }
        catch (err) {
            return fail(err);
        }
    });
    const tagEntity = mcpTool('tag_entity', 'Add tags to an entity. Idempotent — already assigned tags are skipped. Creates tags if they do not exist. Replaces the entity tag set with the union of existing + new.', {
        type: entityTypeSchema,
        slug: z.string(),
        tags: z.array(z.string()),
    }, async (args) => {
        try {
            const type = validateActiveType(String(args.type));
            const slug = String(args.slug);
            const newTags = args.tags;
            const id = pluginHost.resolveEntityId(type, slug);
            if (!id)
                throw new DomainError('NOT_FOUND', `${type} '${slug}' not found`);
            const existing = deps.tagsService.getEntityTagSlugs(type, id);
            const union = [...new Set([...existing, ...newTags])];
            deps.tagsService.assignTags(type, id, union);
            deps.ws.broadcast({ kind: 'entity:changed', entityType: type, slug });
            return ok({ tagged: true, addedCount: union.length - existing.length });
        }
        catch (err) {
            return fail(err);
        }
    });
    const untagEntity = mcpTool('untag_entity', 'Remove tags from an entity.', {
        type: entityTypeSchema,
        slug: z.string(),
        tags: z.array(z.string()),
    }, async (args) => {
        try {
            const type = validateActiveType(String(args.type));
            const slug = String(args.slug);
            const toRemove = new Set(args.tags);
            const id = pluginHost.resolveEntityId(type, slug);
            if (!id)
                throw new DomainError('NOT_FOUND', `${type} '${slug}' not found`);
            const existing = deps.tagsService.getEntityTagSlugs(type, id);
            const remaining = existing.filter((s) => !toRemove.has(s));
            deps.tagsService.assignTags(type, id, remaining);
            deps.ws.broadcast({ kind: 'entity:changed', entityType: type, slug });
            return ok({ untagged: true, removedCount: existing.length - remaining.length });
        }
        catch (err) {
            return fail(err);
        }
    });
    const findReferences = mcpTool('find_references', 'Find all pages that reference a specific entity. Static refs match by (type, slug); when `includeTagMatches` is true, dynamic refs are also reported — pages with <tagged_list/> or <tagged_list_mixed/> whose `tags` attribute intersects the entity\'s tag set (rows include `via: string[]` listing matched tags). Use to understand where an entity is used before modifying or deleting it.', {
        type: entityTypeSchema,
        slug: z.string(),
        includeTagMatches: z.boolean().optional(),
    }, async (args) => {
        try {
            const type = validateActiveType(String(args.type));
            const slug = String(args.slug);
            const includeTagMatches = args.includeTagMatches === true;
            const hits = await deps.referencesService.findReferences(type, slug);
            const references = hits.map((h) => ({ pagePath: h.pagePath, tagType: h.tagType, line: h.line }));
            if (includeTagMatches) {
                const id = pluginHost.resolveEntityId(type, slug);
                if (id) {
                    const entityTagSlugs = deps.tagsService.getEntityTagSlugs(type, id);
                    const entityTags = new Set(entityTagSlugs);
                    if (entityTags.size > 0) {
                        const tree = await deps.pagesService.listTree();
                        const pagePaths = [];
                        (function collect(nodes) {
                            for (const n of nodes) {
                                if (n.type === 'file')
                                    pagePaths.push(n.path);
                                else if (n.children)
                                    collect(n.children);
                            }
                        })(tree);
                        for (const p of pagePaths) {
                            const page = await deps.pagesService.read(p);
                            for (const tag of parseXmlTagsExcludingCode(page.body)) {
                                const via = taggedListVia(tag, type, entityTags);
                                if (via.length === 0)
                                    continue;
                                references.push({
                                    pagePath: p,
                                    tagType: tag.kind,
                                    line: tag.line,
                                    via,
                                });
                            }
                        }
                    }
                }
            }
            return ok({ references });
        }
        catch (err) {
            return fail(err);
        }
    });
    const checkConsistency = mcpTool('check_consistency', 'Run a full consistency check across pages, entities, and tags. Reports broken references in 3 categories (broken-reference / inactive-plugin / unknown-type), plus orphaned entity_tag rows, unreferenced entities, broken extension references (rule 8 — e.g. <section_ref/> with unknown anchor), broken AC verifies (rule 9 — always on when AC plugin active), entity-without-AC-coverage (rule 10 — config-flagged via config.consistency.requireAcCoverage), module-without-AC (rule 11 — config-flagged via config.consistency.requireModuleAc).', {}, async () => {
        try {
            // Build slug sets generically — iterate the host's available plugins and
            // pull rows from SQLite via the shared RawEntityReader. Active vs.
            // inactive distinction comes from `pluginHost.getEntity(type)`.
            // Listed types stay in scope for unreferenced-entity reporting below.
            const reader = new RawEntityReader(deps.db);
            const slugSetsByType = {};
            const referencedByType = {};
            const entitiesByType = {};
            // Per-entity tag sets, used by rule 3 to mark tag-driven references
            // (<tagged_list>/<tagged_list_mixed>) via the shared taggedListVia predicate.
            const entityTagsByType = {};
            for (const m of pluginHost.listEntities()) {
                if (!isRawEntityType(m.type))
                    continue;
                const slugs = reader.listSlugs(m.type);
                entitiesByType[m.type] = slugs.map((s) => ({ slug: s }));
                slugSetsByType[m.type] = new Set(slugs);
                referencedByType[m.type] = new Set();
                const tagMap = new Map();
                for (const s of slugs) {
                    const id = pluginHost.resolveEntityId(m.type, s);
                    tagMap.set(s, new Set(id ? deps.tagsService.getEntityTagSlugs(m.type, id) : []));
                }
                entityTagsByType[m.type] = tagMap;
            }
            const tagSlugs = new Set(deps.tagsService.list().map((t) => t.slug));
            const brokenReferences = [];
            const invalidTagReferences = [];
            const brokenExtensionReferences = [];
            const tree = await deps.pagesService.listTree();
            const pagePaths = [];
            (function collect(nodes) {
                for (const n of nodes) {
                    if (n.type === 'file')
                        pagePaths.push(n.path);
                    else if (n.children)
                        collect(n.children);
                }
            })(tree);
            const categorise = (type) => {
                if (pluginHost.getEntity(type))
                    return 'active';
                if (pluginHost.getAvailable(type))
                    return 'inactive-plugin';
                return 'unknown-type';
            };
            for (const p of pagePaths) {
                const page = await deps.pagesService.read(p);
                for (const tag of parseXmlTagsExcludingCode(page.body)) {
                    const tagType = tag.attrs.type;
                    if (tag.kind !== 'tagged_list_mixed' && tagType) {
                        const cat = categorise(tagType);
                        const slugs = tag.kind === 'element_list'
                            ? (tag.attrs.slugs ?? '').split(',').map((s) => s.trim()).filter(Boolean)
                            : tag.attrs.slug
                                ? [tag.attrs.slug]
                                : [];
                        if (cat !== 'active') {
                            // Whole tag is broken because the type itself is not addressable.
                            for (const s of slugs) {
                                brokenReferences.push({
                                    pagePath: p,
                                    tagType: tag.kind,
                                    type: tagType,
                                    slug: s,
                                    line: tag.line,
                                    category: cat,
                                });
                            }
                            continue;
                        }
                        const set = slugSetsByType[tagType];
                        const referenced = referencedByType[tagType];
                        if (!set)
                            continue; // Active plugin but not raw-readable (shouldn't happen).
                        for (const s of slugs) {
                            if (set.has(s)) {
                                referenced?.add(s);
                            }
                            else {
                                brokenReferences.push({
                                    pagePath: p,
                                    tagType: tag.kind,
                                    type: tagType,
                                    slug: s,
                                    line: tag.line,
                                    category: 'broken-reference',
                                });
                            }
                        }
                    }
                    if (tag.kind === 'tagged_list' || tag.kind === 'tagged_list_mixed') {
                        for (const t of (tag.attrs.tags ?? '').split(',').map((x) => x.trim()).filter(Boolean)) {
                            if (!tagSlugs.has(t))
                                invalidTagReferences.push({ pagePath: p, tagType: tag.kind, tag: t, line: tag.line });
                        }
                        // Rule 3 — tag-driven references: mark every entity whose tag set intersects
                        // this tagged_list/tagged_list_mixed, via the shared taggedListVia predicate.
                        const candidateTypes = tag.kind === 'tagged_list'
                            ? (tag.attrs.type ? [tag.attrs.type] : [])
                            : Object.keys(entityTagsByType);
                        for (const t of candidateTypes) {
                            const tagMap = entityTagsByType[t];
                            const referenced = referencedByType[t];
                            if (!tagMap || !referenced)
                                continue;
                            for (const [slug, etags] of tagMap) {
                                if (taggedListVia(tag, t, etags).length > 0)
                                    referenced.add(slug);
                            }
                        }
                    }
                    // Rule 8 — broken extension reference (e.g. <section_ref/> with unknown anchor).
                    // Validation is delegated to the slot owner via the registered `validate` callback;
                    // M19 does not know section_index. Owners surface a `category` like 'unknown-anchor'.
                    if (tag.source === 'extension') {
                        const ext = listExtensionReferenceTypes().find((e) => e.tag === tag.kind);
                        if (ext?.validate) {
                            const result = ext.validate(tag.attrs);
                            if (!result.ok) {
                                brokenExtensionReferences.push({
                                    pagePath: p,
                                    tagType: tag.kind,
                                    attrs: tag.attrs,
                                    line: tag.line,
                                    category: result.category,
                                });
                            }
                        }
                    }
                }
            }
            const unreferencedEntities = [];
            for (const [type, list] of Object.entries(entitiesByType)) {
                const referenced = referencedByType[type];
                if (!referenced)
                    continue;
                for (const e of list)
                    if (!referenced.has(e.slug))
                        unreferencedEntities.push({ type, slug: e.slug });
            }
            // Rules 9/10/11 — AC-specific. Silent skip when AC plugin is not active
            // (host.getEntity('ac') === null) so projects without AC don't crash.
            const brokenAcVerifies = [];
            const entitiesWithoutAcCoverage = [];
            const modulesWithoutAc = [];
            const acActive = pluginHost.getEntity('ac');
            if (acActive) {
                const acService = pluginHost.getEntityService('ac');
                const config = readConfig(deps.cwd);
                const requireAcCoverage = config.consistency?.requireAcCoverage ?? 'off';
                const requireModuleAc = config.consistency?.requireModuleAc ?? 'off';
                if (acService) {
                    const activeAcs = acService.list({ status: 'active' });
                    // Rule 9 — broken AC verifies. AcService.classifyVerifies already
                    // categorises into missing|inactive|unknown via the plugin host.
                    for (const ac of activeAcs) {
                        const broken = acService.classifyVerifies(ac.verifies);
                        for (const b of broken) {
                            brokenAcVerifies.push({
                                acSlug: ac.slug,
                                verifyType: b.type,
                                verifySlug: b.slug,
                                category: b.reason,
                            });
                        }
                    }
                    // Rule 10 — entity-without-AC-coverage. Coverage = at least one AC
                    // either lists the entity in `verifies[]` or carries an `entity-{slug}` tag.
                    if (requireAcCoverage !== 'off') {
                        const coveredByVerifies = new Set();
                        const coveredByTag = new Set();
                        for (const ac of activeAcs) {
                            for (const v of ac.verifies) {
                                coveredByVerifies.add(`${v.type}:${v.slug}`);
                            }
                            for (const t of ac.tags) {
                                if (t.startsWith('entity-')) {
                                    coveredByTag.add(t.slice('entity-'.length));
                                }
                            }
                        }
                        for (const [type, list] of Object.entries(entitiesByType)) {
                            if (type === 'ac')
                                continue;
                            for (const e of list) {
                                const key = `${type}:${e.slug}`;
                                if (coveredByVerifies.has(key))
                                    continue;
                                if (coveredByTag.has(e.slug))
                                    continue;
                                entitiesWithoutAcCoverage.push({
                                    type,
                                    slug: e.slug,
                                    severity: requireAcCoverage,
                                });
                            }
                        }
                    }
                    // Rule 11 — module-without-AC. Module = `mNN` derived from a
                    // `modules/mNN-…\.md` page path. Coverage = AC carrying that mNN tag.
                    if (requireModuleAc !== 'off') {
                        const moduleRe = /modules\/(m\d{2})-[^/]+\.md$/;
                        const modules = new Set();
                        for (const p of pagePaths) {
                            const m = moduleRe.exec(p);
                            if (m && m[1])
                                modules.add(m[1]);
                        }
                        const taggedModules = new Set();
                        for (const ac of activeAcs) {
                            for (const t of ac.tags) {
                                if (/^m\d{2}$/.test(t))
                                    taggedModules.add(t);
                            }
                        }
                        for (const mod of modules) {
                            if (!taggedModules.has(mod)) {
                                modulesWithoutAc.push({ module: mod, severity: requireModuleAc });
                            }
                        }
                    }
                }
            }
            const acErrorRows = brokenAcVerifies.length +
                entitiesWithoutAcCoverage.filter((e) => e.severity === 'error').length +
                modulesWithoutAc.filter((m) => m.severity === 'error').length;
            const acWarningRows = entitiesWithoutAcCoverage.filter((e) => e.severity === 'warn').length +
                modulesWithoutAc.filter((m) => m.severity === 'warn').length;
            const errors = brokenReferences.length +
                invalidTagReferences.length +
                brokenExtensionReferences.length +
                acErrorRows;
            const warnings = unreferencedEntities.length + acWarningRows;
            const counts = brokenReferences.reduce((acc, r) => {
                acc[r.category] = (acc[r.category] ?? 0) + 1;
                return acc;
            }, {});
            const extensionCounts = brokenExtensionReferences.reduce((acc, r) => {
                const key = `${r.tagType}:${r.category}`;
                acc[key] = (acc[key] ?? 0) + 1;
                return acc;
            }, {});
            const acVerifyCounts = brokenAcVerifies.reduce((acc, r) => {
                acc[r.category] = (acc[r.category] ?? 0) + 1;
                return acc;
            }, {});
            return ok({
                brokenReferences,
                brokenReferenceCounts: counts,
                orphanedEntityTags: [],
                unreferencedEntities,
                invalidTagReferences,
                brokenExtensionReferences,
                brokenExtensionReferenceCounts: extensionCounts,
                brokenAcVerifies,
                brokenAcVerifyCounts: acVerifyCounts,
                entitiesWithoutAcCoverage,
                modulesWithoutAc,
                summary: { total: errors + warnings, errors, warnings },
            });
        }
        catch (err) {
            return fail(err);
        }
    });
    const analyzeAcAgainstEntities = mcpTool('analyze_ac_against_entities', 'LLM-based on-demand semantic check: for each active AC, load its `text` + `verifies[]` + the linked entity snapshots and ask the model whether the AC text matches the shape of those entities. Non-deterministic and expensive — call deliberately, not in a loop. Distinct from `check_consistency` (which is deterministic and structural). Output: { issues: [{ ac_slug, issue_type, details, affected_entity?, confidence, suggested_correction? }] }.', {
        acSlug: z.string().optional().describe('Limit analysis to a single AC by slug. Omit to analyse all active ACs.'),
    }, async (args) => {
        try {
            if (!pluginHost.getEntity('ac')) {
                throw new DomainError('VALIDATION', 'ac plugin is not active in this project');
            }
            const acService = pluginHost.getEntityService('ac');
            if (!acService)
                throw new DomainError('INTERNAL', 'ac service not registered');
            const filterSlug = args.acSlug ? String(args.acSlug) : undefined;
            const allActive = acService.list({ status: 'active' });
            const targets = filterSlug ? allActive.filter((a) => a.slug === filterSlug) : allActive;
            if (targets.length === 0) {
                return ok({ issues: [] });
            }
            const reader = new RawEntityReader(deps.db);
            const dossier = targets.map((ac) => {
                const linked = ac.verifies.map((v) => {
                    if (!isRawEntityType(v.type)) {
                        return { type: v.type, slug: v.slug, status: 'unknown-type' };
                    }
                    const entity = reader.getEntity(v.type, v.slug);
                    if (!entity) {
                        return { type: v.type, slug: v.slug, status: 'missing' };
                    }
                    return {
                        type: v.type,
                        slug: v.slug,
                        status: 'active',
                        data: entity.data,
                    };
                });
                return { slug: ac.slug, text: ac.text, kind: ac.kind, linked };
            });
            const prompt = [
                'You are a specification consistency auditor.',
                '',
                'For each Acceptance Criterion (AC) below, decide whether its `text` is semantically consistent with the linked entities (their fields, params, shape).',
                '',
                'Return ONLY a JSON object on a single line, no prose, matching:',
                '{"issues":[{"ac_slug":string,"issue_type":string,"details":string,"affected_entity"?:{"type":string,"slug":string},"confidence":number,"suggested_correction"?:string}]}',
                '',
                'Rules:',
                '- If an AC has no issues, do not emit a row for it.',
                '- `confidence` is between 0 and 1.',
                '- `issue_type` is a short kebab-case label (e.g. "field-mismatch", "verb-mismatch", "missing-coverage", "stale-shape").',
                '- Skip ACs whose linked entities are missing or unknown-type (those are caught by check_consistency rule 9).',
                '',
                'Dossier:',
                JSON.stringify(dossier),
            ].join('\n');
            const adapter = createAdapter('claude-code');
            const stream = adapter.execute({
                prompt,
                systemPrompt: 'You output only a single JSON object on one line. No commentary, no code fences.',
                model: 'sonnet-4.6',
                cwd: deps.cwd,
                maxTurns: 1,
            });
            const text = await extractText(stream);
            const issues = parseIssuesJson(text);
            return ok({ issues });
        }
        catch (err) {
            return fail(err);
        }
    });
    const listSections = mcpTool('list_sections', 'List sections from the section index. Filter by `anchor` (exact match), `query` (substring match on heading_text/heading_path), or `pagePath` (sections of a single page). Thin proxy over SectionsService — `section_index` is owned by M06.', {
        anchor: z.string().optional(),
        query: z.string().optional(),
        pagePath: z.string().optional(),
        limit: z.number().int().positive().max(2000).optional(),
    }, async (args) => {
        try {
            const anchor = args.anchor ? String(args.anchor) : undefined;
            if (anchor) {
                const entry = deps.sectionsService.getByAnchor(anchor);
                return ok({
                    sections: entry
                        ? [
                            {
                                anchor: entry.anchor,
                                pagePath: entry.pagePath,
                                headingText: entry.headingText,
                                headingPath: entry.headingPath,
                                headingLevel: entry.headingLevel,
                                lineStart: entry.lineStart,
                                lineEnd: entry.lineEnd,
                            },
                        ]
                        : [],
                });
            }
            const entries = deps.sectionsService.list({
                pagePath: args.pagePath ? String(args.pagePath) : undefined,
                search: args.query ? String(args.query) : undefined,
                limit: args.limit,
            });
            return ok({
                sections: entries.map((e) => ({
                    anchor: e.anchor,
                    pagePath: e.pagePath,
                    headingText: e.headingText,
                    headingPath: e.headingPath,
                    headingLevel: e.headingLevel,
                    lineStart: e.lineStart,
                    lineEnd: e.lineEnd,
                })),
            });
        }
        catch (err) {
            return fail(err);
        }
    });
    return createMcpServer({
        name: 'reference-tools',
        tools: [
            createTag,
            updateTag,
            deleteTag,
            listTags,
            tagEntity,
            untagEntity,
            findReferences,
            checkConsistency,
            analyzeAcAgainstEntities,
            listSections,
        ],
    });
}
function parseIssuesJson(text) {
    const start = text.indexOf('{');
    const end = text.lastIndexOf('}');
    if (start < 0 || end <= start)
        return [];
    const slice = text.slice(start, end + 1);
    let parsed;
    try {
        parsed = JSON.parse(slice);
    }
    catch {
        return [];
    }
    if (!parsed || typeof parsed !== 'object')
        return [];
    const rawIssues = parsed.issues;
    if (!Array.isArray(rawIssues))
        return [];
    const out = [];
    for (const raw of rawIssues) {
        if (!raw || typeof raw !== 'object')
            continue;
        const r = raw;
        if (typeof r.ac_slug !== 'string' || typeof r.issue_type !== 'string')
            continue;
        const details = typeof r.details === 'string' ? r.details : '';
        const confidence = typeof r.confidence === 'number' ? r.confidence : 0;
        const issue = {
            ac_slug: r.ac_slug,
            issue_type: r.issue_type,
            details,
            confidence,
        };
        if (r.affected_entity && typeof r.affected_entity === 'object') {
            const ae = r.affected_entity;
            if (typeof ae.type === 'string' && typeof ae.slug === 'string') {
                issue.affected_entity = { type: ae.type, slug: ae.slug };
            }
        }
        if (typeof r.suggested_correction === 'string') {
            issue.suggested_correction = r.suggested_correction;
        }
        out.push(issue);
    }
    return out;
}
//# sourceMappingURL=reference-tools.js.map