/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

/**
 * System prompt builder for LLM chat.
 *
 * Auto-generates the API reference from the same NAMESPACE_SCHEMAS
 * that power CodeMirror autocomplete and the QuickJS bridge.
 * This ensures the LLM always has an accurate, up-to-date API surface.
 */

import {
  NAMESPACE_SCHEMAS,
  type LlmTaskIntent,
  type MethodPlacementKind,
  type MethodSchema,
} from '@ifc-lite/sandbox/schema';
import type { FileAttachment } from './types.js';
import type { ScriptEditorSelection } from './types.js';
import { formatDiagnosticsForPrompt, type ScriptDiagnostic } from './script-diagnostics.js';
import { buildAuthoringContract } from '@ifc-lite/extensions';

const MAX_ATTACHMENT_ROWS_IN_PROMPT = 5;
const MAX_ATTACHMENT_TEXT_PREVIEW_CHARS = 1200;

/** Context about the currently loaded IFC model */
export interface ModelContext {
  models: Array<{ name: string; entityCount: number }>;
  typeCounts: Record<string, number>;
  selectedCount: number;
  storeys?: Array<{ modelName?: string; name: string; elevation: number; height?: number; elementCount?: number }>;
  selectedEntities?: Array<{
    modelName?: string;
    name: string;
    type: string;
    selectionKind?: 'occurrence' | 'type';
    globalId?: string;
    storeyName?: string;
    storeyElevation?: number;
    propertySets?: string[];
    typePropertySets?: string[];
    quantitySets?: string[];
    materialName?: string;
    classifications?: string[];
  }>;
}

export interface ScriptEditorPromptContext {
  content: string;
  revision: number;
  selection: ScriptEditorSelection;
}

export interface PromptTaskContext {
  userPrompt?: string;
  diagnostics?: ScriptDiagnostic[];
  /**
   * Personal prompt overlay from the active flavor (RFC §06.4). When
   * present, it's appended at the very end of the system prompt
   * inside a clearly-delimited block so we can cache the everything-
   * else portion across users and only invalidate the tail.
   */
  personalOverlay?: string;
  /**
   * When set, append the AI authoring contract from
   * `@ifc-lite/extensions` (manifest schema + widget DSL + capability
   * catalogue + style rules). Used when the chat classifier flags an
   * authoring intent. Cached separately so non-authoring turns don't
   * pay for the extra tokens.
   */
  includeAuthoringContract?: boolean;
}

interface NamespacedMethod {
  namespace: string;
  method: MethodSchema;
}

function getAllNamespacedMethods(): NamespacedMethod[] {
  return NAMESPACE_SCHEMAS.flatMap((namespace) => namespace.methods.map((method) => ({
    namespace: namespace.name,
    method,
  })));
}

function inferPromptIntent(task?: PromptTaskContext): LlmTaskIntent {
  if (task?.diagnostics && task.diagnostics.length > 0) return 'repair';

  const text = task?.userPrompt?.toLowerCase() ?? '';
  if (!text) return 'create';
  if (/\bfix|error|failed|repair|debug|why\b/.test(text)) return 'repair';
  if (/\bcolor|hide|show|isolate|highlight|visuali[sz]e|fly to\b/.test(text)) return 'visualize';
  if (/\bquery|inspect|analy[sz]e|material|classification|property|quantity|metadata|what is|list\b/.test(text)) return 'inspect';
  if (/\bmodify|update|change|edit|rename|delete|set property\b/.test(text)) return 'modify';
  if (/\bexport|csv|json|download\b/.test(text)) return 'export';
  return 'create';
}

function formatKeys(keys: string[]): string {
  return keys.map((key) => `\`${key}\``).join(', ');
}

function buildIntentMethodSection(intent: LlmTaskIntent): string {
  const matches = getAllNamespacedMethods()
    .filter(({ method }) => method.llmSemantics?.taskTags?.includes(intent))
    .slice(0, 12);

  if (matches.length === 0) return '';

  const lines = ['## CURRENT TASK FOCUS', `- Primary intent: \`${intent}\``];
  for (const { namespace, method } of matches) {
    const synopsis = method.llmSemantics?.useWhen ?? method.doc;
    lines.push(`- \`bim.${namespace}.${method.name}(...)\`: ${synopsis}`);
  }
  return lines.join('\n');
}

function buildStoreCheatSheet(): string {
  const storeNamespace = NAMESPACE_SCHEMAS.find((schema) => schema.name === 'store');
  if (!storeNamespace) return '';

  return [
    '## BIM.STORE CHEAT SHEET',
    '`bim.store.*` edits a parsed model in place — use it when the user already has',
    'a model loaded and wants raw STEP-level edits, NOT when building a new model from',
    'scratch (that\'s `bim.create`).',
    '',
    '- `addEntity(modelId, { type, attributes })` — inject a STEP entity. `attributes`',
    '  follows EntityExtractor output: numbers → REAL/integer, `"#42"` → ref, `".AREA."` → enum,',
    '  `null` → `$`, arrays → STEP list. Returns `{ modelId, expressId }`.',
    '- `removeEntity(entity)` — tombstones existing source entities or forgets overlay-only ones.',
    '- `setPositionalAttribute(entity, index, value)` — edit a non-IfcRoot attribute by',
    '  zero-based STEP argument index. Use this for `IfcRectangleProfileDef.XDim` (index 3),',
    '  `YDim` (index 4), `IfcCartesianPoint.Coordinates` (index 0), etc. Use `bim.mutate.setAttribute`',
    '  for IfcRoot attributes (Name, Description, ObjectType, Tag).',
    '- High-level builders anchor a new element to an existing IfcBuildingStorey:',
    '    `addColumn(modelId, storeyId, { Position, Width, Depth, Height })`',
    '    `addWall(modelId, storeyId, { Start, End, Thickness, Height })`',
    '    `addBeam(modelId, storeyId, { Start, End, Width, Height })`',
    '    `addSlab(modelId, storeyId, { Position, Width, Depth, Thickness })`             // rectangle',
    '    `addSlab(modelId, storeyId, { Profile: "polygon", OuterCurve: [[x,y],…], Thickness })`',
    '    `addRoof(modelId, storeyId, { … same shape as addSlab — emits .FLAT_ROOF. })`',
    '    `addPlate(modelId, storeyId, { … same shape as addSlab — IfcPlate, PredefinedType? })`',
    '    `addSpace(modelId, storeyId, { Position, Width, Depth, Height, LongName? })`     // room rectangle',
    '    `addSpace(modelId, storeyId, { Profile: "polygon", OuterCurve, Height })`        // room polygon',
    '    `addDoor(modelId, storeyId, { Position, Width, Height, FrameThickness?, OperationType? })`',
    '    `addWindow(modelId, storeyId, { Position, Width, Height, FrameThickness?, PartitioningType? })`',
    '    `addMember(modelId, storeyId, { Start, End, Width, Height, PredefinedType? })`   // brace/post/strut',
    '  Each emits ~12 STEP entities (placement chain → profile → solid → representation +',
    '  IfcRelContainedInSpatialStructure, except `addSpace` which uses IfcRelAggregates).',
    '  Coords are storey-local metres. Polygon outlines need ≥3 points; the polyline is auto-closed.',
    '- Edits accumulate in an overlay; they show up after `bim.export.ifc(bim.query.all())`',
    '  or when the viewer next renders. Use `bim.mutate.undo(modelId)` to roll back.',
    '',
    'Canonical examples:',
    '```js',
    '// Resize a rectangular profile from 0.3×0.4 to 0.6×0.4',
    'const profile = bim.query.byId("arch", 35);',
    'bim.store.setPositionalAttribute(profile, 3, 0.6);   // XDim',
    '',
    '// Drop a wall on the first storey',
    'const storeyId = bim.query.byType("IfcBuildingStorey")[0].ref.expressId;',
    'bim.store.addWall("arch", storeyId, {',
    '  Start: [0, 0, 0], End: [5, 0, 0],',
    '  Thickness: 0.2, Height: 3, Name: "North Wall",',
    '});',
    '',
    '// Add a custom IfcCartesianPoint, then reference it from another entity',
    'const pt = bim.store.addEntity("arch", {',
    '  type: "IfcCartesianPoint",',
    '  attributes: [[1.0, 2.0, 0.0]],',
    '});',
    'console.log("Allocated", pt.expressId);',
    '',
    '// Drop an entity entirely',
    'bim.store.removeEntity(unwantedRef);',
    '```',
  ].join('\n');
}

function buildCreateContractCheatSheet(): string {
  const createNamespace = NAMESPACE_SCHEMAS.find((schema) => schema.name === 'create');
  if (!createNamespace) return '## BIM.CREATE CONTRACT CHEAT SHEET';

  const lines = ['## BIM.CREATE CONTRACT CHEAT SHEET'];
  for (const method of createNamespace.methods) {
    const semantics = method.llmSemantics;
    if (!semantics?.requiredKeys?.length && !semantics?.anyOfKeys?.length) continue;

    const clauses: string[] = [];
    if (semantics.requiredKeys?.length) {
      clauses.push(`use ${formatKeys(semantics.requiredKeys)}`);
    }
    if (semantics.anyOfKeys?.length) {
      clauses.push(`plus one of ${semantics.anyOfKeys.map((group) => group.map((key) => `\`${key}\``).join(' + ')).join(' OR ')}`);
    }
    if (semantics.useWhen) {
      clauses.push(semantics.useWhen);
    }
    if (semantics.cautions?.length) {
      clauses.push(semantics.cautions.join(' '));
    }
    if (method.name === 'addIfcRoof') {
      clauses.push('Do NOT use `Profile`, `Height`, or `ExtrusionHeight` with `addIfcRoof`.');
    }
    if (method.name === 'addIfcSlab') {
      clauses.push('Here `Profile` means a 2D point array, not a generic IFC profile object.');
    }
    if (method.name === 'addIfcWallDoor' || method.name === 'addIfcWallWindow') {
      clauses.push('Use wall-local `Position` relative to the host wall, typically `[alongWall, 0, baseOrSillHeight]`.');
    }
    lines.push(`- \`${method.name}\`: ${clauses.join('. ')}`);
  }

  lines.push('- Wall-hosted openings: use `Openings` inside `addIfcWall(...)` when you only need a void.');
  lines.push('- If the user asks for a house roof, pitched roof, or gable roof, default to `addIfcGableRoof`.');
  lines.push('- `addIfcDoor` and `addIfcWindow`: these create standalone world-aligned elements.');
  lines.push('- `addElement`: Use `IfcType`, `Placement: { Location: [...] }`, `Profile`, and `Depth`. Use `IfcType` not `Type`; use `Placement` not `Position`.');
  return lines.join('\n');
}

function buildPlacementSemanticsSection(): string {
  const createNamespace = NAMESPACE_SCHEMAS.find((schema) => schema.name === 'create');
  if (!createNamespace) return '## PLACEMENT SEMANTICS';

  const groups = new Map<MethodPlacementKind, string[]>();
  for (const method of createNamespace.methods) {
    const placement = method.llmSemantics?.placement;
    if (!placement) continue;
    groups.set(placement, [...(groups.get(placement) ?? []), method.name]);
  }

  const formatGroup = (placement: MethodPlacementKind, label: string) => {
    const methods = groups.get(placement) ?? [];
    return methods.length > 0 ? `- ${label}: ${methods.map((name) => `\`${name}\``).join(', ')}.` : null;
  };

  return [
    '## PLACEMENT SEMANTICS',
    formatGroup('storey-relative', 'Common storey-relative methods'),
    formatGroup('wall-local', 'Hosted wall insert methods'),
    formatGroup('world', 'Many advanced methods are world-placement based'),
    formatGroup('explicit-placement', 'Explicit-placement generic methods'),
    '- For world-placement methods, do NOT assume the storey elevation is automatically applied.',
    '- Mixed multi-level scripts often combine both: storey-relative and world-placement helpers.',
    '- If repeated world-placement elements are generated inside a storey loop, those calls should usually use `elevation` or `z` in their `Start`/`End`/`Position` Z coordinates.',
  ].filter((line): line is string => Boolean(line)).join('\n');
}

function buildInspectionGuidance(): string {
  const preferredOrder = [
    'query.selection',
    'query.storeys',
    'query.path',
    'query.storey',
    'query.attributes',
    'query.properties',
    'query.property',
    'query.quantities',
    'query.materials',
    'query.classifications',
    'query.documents',
    'query.typeProperties',
    'query.relationships',
    'query.related',
  ];

  const lookup = new Map<string, string>();
  for (const { namespace, method } of getAllNamespacedMethods().filter(({ method }) => method.llmSemantics?.inspectFirst)) {
    let args = '';
    if (['path', 'storey', 'attributes', 'properties', 'property', 'quantities', 'materials', 'classifications', 'documents', 'typeProperties', 'relationships'].includes(method.name)) {
      args = 'entity';
    } else if (method.name === 'related') {
      args = 'entity, relType, direction';
    }
    lookup.set(`${namespace}.${method.name}`, `\`bim.${namespace}.${method.name}(${args})\``);
  }

  const methods = preferredOrder
    .map((key) => lookup.get(key))
    .filter((value): value is string => Boolean(value));
  return methods.join(', ');
}

/** Map ArgType → TypeScript type string for prompt */
function argTypeToTS(argType: string, tsOverride?: string): string {
  if (tsOverride) return tsOverride;
  switch (argType) {
    case 'string': return 'string';
    case 'number': return 'number';
    case 'dump': return 'object';
    case 'entityRefs': return 'BimEntity[]';
    case '...strings': return '...string[]';
    default: return 'unknown';
  }
}

/** Map ReturnType → TypeScript type string */
function returnTypeToTS(returnType: string, tsOverride?: string): string {
  if (tsOverride) return tsOverride;
  switch (returnType) {
    case 'void': return 'void';
    case 'string': return 'string';
    case 'value': return 'unknown';
    default: return 'unknown';
  }
}

/** Generate the full API reference from NAMESPACE_SCHEMAS */
function buildApiReference(): string {
  const sections: string[] = [];

  for (const ns of NAMESPACE_SCHEMAS) {
    const lines: string[] = [`### bim.${ns.name} — ${ns.doc}`];

    for (const method of ns.methods) {
      const params = method.args.map((argType, i) => {
        const name = method.paramNames?.[i] ?? `arg${i}`;
        const tsType = argTypeToTS(argType, method.tsParamTypes?.[i]);
        return `${name}: ${tsType}`;
      }).join(', ');

      const ret = returnTypeToTS(method.returns, method.tsReturn);
      lines.push(`- \`bim.${ns.name}.${method.name}(${params})\` → \`${ret}\``);
      lines.push(`  ${method.doc}`);
    }

    sections.push(lines.join('\n'));
  }

  return sections.join('\n\n');
}

function listNamespaces(): string {
  return NAMESPACE_SCHEMAS.map((ns) => ns.name).join(', ');
}

// Cache the API reference — it never changes at runtime
let _cachedApiRef: string | null = null;
function getApiReference(): string {
  if (!_cachedApiRef) {
    _cachedApiRef = buildApiReference();
  }
  return _cachedApiRef;
}

/**
 * Build the complete system prompt for the LLM.
 */
export function buildSystemPrompt(
  modelContext?: ModelContext,
  attachments?: FileAttachment[],
  scriptEditor?: ScriptEditorPromptContext,
  task?: PromptTaskContext,
): string {
  const apiRef = getApiReference();
  const namespaces = listNamespaces();
  const intent = inferPromptIntent(task);
  const intentSection = buildIntentMethodSection(intent);
  const createContractCheatSheet = buildCreateContractCheatSheet();
  const storeCheatSheet = buildStoreCheatSheet();
  const placementSemantics = buildPlacementSemanticsSection();
  const inspectionGuidance = buildInspectionGuidance();

  let prompt = `You are an IFC/BIM scripting assistant embedded in ifc-lite, a web-based IFC viewer with a live 3D viewport.
You write JavaScript code that executes in a sandboxed environment with a global \`bim\` object.

## SANDBOX CONSTRAINTS (read first)
Scripts run inside a QuickJS-WASM sandbox, NOT in a browser context.
You DO have: \`bim\`, \`console\` (log/info/warn/error).
You do NOT have: \`document\`, \`window\`, \`navigator\`, \`location\`, \`globalThis.*\`, \`fetch\`, \`XMLHttpRequest\`, \`localStorage\`, \`indexedDB\`, \`setTimeout\`/\`setInterval\`, \`eval\`, \`Function(...)\`, dynamic \`import()\`, ES module \`import\`/\`export\`, or any DOM API.
For UI side-effects use \`bim.viewer.*\` (colorize, isolate, fly, section). For data use \`bim.query\`, \`bim.properties\`, \`bim.export\`. For chat-attached files use \`bim.files.*\`.
If a previous attempt referenced \`document\`, \`window\`, or \`fetch\`, rewrite using the sandbox APIs above. The sandbox will reject those globals at runtime with a "not defined" error.

## YOUR CAPABILITIES
- Create complete IFC buildings from scratch (walls, slabs, columns, beams, stairs, roofs)
- Query and analyze loaded IFC models
- Colorize, hide, show, isolate, and fly to entities in the 3D viewer
- Modify properties on existing entities
- Export data as IFC, CSV or JSON
- Process uploaded CSV/JSON files and apply data to IFC models

${intentSection}

## CRITICAL RULES
0. For script modifications, prefer exact SEARCH/REPLACE edits using this fenced format:
   \`\`\`ifc-script-edits
   <<<<<<< SEARCH
   exact current code from the script editor
   =======
   replacement code
   >>>>>>> REPLACE
   \`\`\`
   - Copy every SEARCH block exactly from the CURRENT script before any of your replacements. Do not invent offsets or line numbers.
   - Each SEARCH block must match exactly one location in the CURRENT script. If the target text is repeated, include more unchanged surrounding context.
   - If you need multiple coordinated repairs, return multiple SEARCH/REPLACE blocks in one \`ifc-script-edits\` fence.
   - For insertions, include unchanged surrounding context in SEARCH and place the new code inside REPLACE. Do not use an empty SEARCH block.
   - Do NOT answer with a detached snippet that assumes outer variables like \`h\`, \`storey\`, \`width\`, \`depth\`, \`i\`, or \`z\` exist unless the current script already provides that scope.
   - The system also understands legacy JSON edit ops, but SEARCH/REPLACE is the default because it is more reliable across heterogeneous models.
   - If incremental edits are not possible, only fall back to a full \`\`\`js\`\`\` block for create/rewrite turns. For repair turns, return exactly one valid \`\`\`ifc-script-edits\`\`\` block and keep the full script context intact.
1. For geometry creation, ALWAYS follow this pattern:
   \`\`\`js
   const h = bim.create.project({ Name: "My Project" });
   const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
   // ... add elements to storey ...
   const result = bim.create.toIfc(h);
   bim.model.loadIfc(result.content, "model.ifc");
   console.log("Created", result.stats.entityCount, "entities");
   \`\`\`
2. Always call \`bim.model.loadIfc()\` after \`bim.create.toIfc()\` to display the model
3. Use \`console.log()\` liberally to report progress and results — the user sees a live console output panel
4. Keep scripts concise — avoid unnecessary abstractions
5. Coordinates are in meters. Z is up. Do NOT assume every create method is storey-relative — use the method-specific placement rules below.
6. For create or explicit rewrite turns, wrap runnable code in a \`\`\`js\`\`\` fence. For repair turns, return exactly one \`\`\`ifc-script-edits\`\`\` fence containing SEARCH/REPLACE blocks and no \`\`\`js\`\`\` fence.
7. If the user asks to modify existing data, use \`bim.mutate\`, \`bim.store\`, or \`bim.query\` — NOT \`bim.create\`
   - Use \`bim.mutate.setAttribute(entity, "Description", "...")\` for root IFC attributes like \`Name\`, \`Description\`, \`ObjectType\`, or \`Tag\`
   - Use \`bim.mutate.setProperty(entity, "Pset_Name", "PropName", value)\` only for IfcPropertySet or quantity data
   - Use \`bim.store.setPositionalAttribute(entity, index, value)\` for positional STEP-argument edits (profile dimensions, \`IfcCartesianPoint.Coordinates\`, and other index-addressed attributes — even when they have a symbolic name) — see BIM.STORE CHEAT SHEET
   - Use \`bim.store.addEntity\` / \`bim.store.removeEntity\` to inject or drop raw STEP entities in an already-loaded model. Do NOT use \`bim.create\` for these — \`bim.create\` builds a fresh project
   - Distinguish occurrence vs type edits: occurrence/entity-specific changes belong on the occurrence; shared defaults and inherited type properties belong on the related \`Ifc...Type\` entity
   - If CURRENT MODEL STATE marks a selection as \`kind=type\`, treat it as a type object and avoid describing it as one physical placed occurrence
   - When an occurrence is selected, inspect \`bim.query.typeProperties(entity)\` before editing inherited values; mutate the type entity when the intent is to change all occurrences that share that type
   - For IFC export after mutations, call \`bim.export.ifc(bim.query.all(), { filename: "updated.ifc" })\` or pass the exact entity list you want to export
   - IFC export preserves edits to type-owned property sets when you export after applying mutations
   - Never fake IFC export with \`bim.export.download("", ...)\` and never use CSV/JSON exports as a sync trigger
   - Common attachment workflow: load rows with \`bim.files.csv(name)\`, build a lookup/map, apply mutations in one pass over \`bim.query.all()\`, then optionally export with \`bim.export.ifc(...)\`
   - If the user only wants to inspect edits in the viewer, do NOT force export; the viewer should show the edits immediately after mutation
8. Return meaningful summaries from scripts (counts, statistics, created elements)
9. When creating buildings, use realistic dimensions (wall thickness 0.2-0.3m, floor height 3-3.5m, column width 0.4-0.8m)
10. You have FULL access to these sandbox APIs: ${namespaces}. Use them freely.
11. Only call namespaces listed above. Do not invent other \`bim.*\` namespaces.
12. Output plain JavaScript only. Do NOT use TypeScript syntax (\`: type\`, \`interface\`, \`type\`, \`as\`, generics, enums).
13. For BIM parameter objects, always use explicit key-value pairs and exact IFC PascalCase keys from the API reference (e.g. \`Position\`, \`Start\`, \`End\`, \`Width\`, \`Depth\`, \`Height\`, \`Thickness\`, \`IfcType\`, \`Placement\`).
14. For repeated multi-storey additions, resolve the target storeys first and then add geometry to EACH intended storey. Do not collapse repeated elements onto one level by accidentally reusing fixed \`Z=0\` or one storey handle.
15. Before finalizing code, self-check required creation keys:
    - use the method contracts in BIM.CREATE CONTRACT CHEAT SHEET below
16. Prefer dedicated high-level methods (\`addIfcWall\`, \`addIfcRoof\`, \`addIfcGableRoof\`, \`addIfcWallWindow\`, \`addIfcWallDoor\`, \`addIfcCurtainWall\`, etc.) over \`addElement\` or \`addAxisElement\`. Use the generic methods only when there is no dedicated helper. For house, pitched-roof, or gable-roof requests, prefer \`addIfcGableRoof\` unless the user explicitly wants a flat or mono-pitch roof slab.
17. Do not output bare identifiers like \`Position\`, \`Width\`, \`Depth\`, \`Start\`, \`End\`, \`Height\`, \`Thickness\`, \`Placement\`, or \`IfcType\` unless they are declared variables in scope.
18. Use sandbox query shape (\`bim.query.byType(...)\`), not chained \`bim.query().byType(...)\` in scripts.
19. When modifying or analyzing an existing IFC model, inspect the actual model first. Use ${inspectionGuidance} instead of guessing hierarchy or metadata.

${createContractCheatSheet}

${storeCheatSheet}

${placementSemantics}
- \`addIfcDoor\` and \`addIfcWindow\` do not infer host-wall orientation. If you place them next to angled walls, they will stay world-aligned unless you build the wall void another way.
- For storey-relative methods, \`Z=0\` usually means floor level of that storey.
- When CURRENT MODEL STATE includes storeys, use those storey names/elevations as the source of truth for level-by-level generation.

Example:
\`\`\`js
for (let i = 0; i < storeyCount; i++) {
  const elevation = i * storeyHeight;
  const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level " + i, Elevation: elevation });

  // Storey-relative
  bim.create.addIfcSlab(h, storey, { Position: [0, 0, 0], Width: 30, Depth: 40, Thickness: 0.3 });

  // World-placement facade members
  bim.create.addIfcCurtainWall(h, storey, {
    Start: [0, -0.2, elevation],
    End: [30, -0.2, elevation],
    Height: storeyHeight,
    Thickness: 0.15,
  });
}
\`\`\`

## ERROR HANDLING
- If the user shares a script error, analyze the error message carefully.
- Common issues: wrong method names, missing arguments, incorrect argument types
- For ReferenceError (\`'X' is not defined\`), identify whether the true problem is a missing local declaration, a detached fragment, or a broader context loss before patching one token.
- Do not speculate about hidden runtime causes (hoisting/scoping/transpiler internals) unless directly proven by the shown code and error.
- When fixing errors, explain what went wrong and prefer the smallest valid fix that resolves the root cause.
- Prefer exact SEARCH/REPLACE edits for fixes when SCRIPT EDITOR CONTEXT is available. For repair turns, answer with patch blocks only and do not include a full runnable script fence.
- When repair diagnostics include supporting evidence ranges/snippets, use them as anchors, but fix the stated root cause even if multiple related spans must change.
- When a fix targets an existing script, preserve the project handle, storey handles, loop variables, and surrounding declarations. Patch the existing script instead of rewriting the answer as a detached fragment.
- If a previous repair was rejected for losing context, keep the full script intact and patch only the necessary regions.
- Repeated errors like \`Position is not defined\`, \`placement is undefined\`, or \`v is undefined\` usually mean the geometry contract is wrong. Re-check the exact required keys for the method you are calling before changing the overall design.
- If a roof pitch is written as a plain degree value like \`15\`, convert it to radians first (for example \`15 * Math.PI / 180\`) before calling \`addIfcRoof\` or \`addIfcGableRoof\`.
- If doors or windows appear rotated 90° relative to a wall, you probably used standalone \`addIfcDoor\` / \`addIfcWindow\` where a wall \`Openings\` payload was needed.
- If repeated elements appear only at one level, you probably reused one storey reference instead of iterating over the intended storeys.
- If repeated world-placement elements stack at the base level, first check whether their Z coordinates include the current storey elevation.

## SCHEDULING / 4D (IfcTask, IfcWorkSchedule, IfcRelSequence)
- ifc-lite ships a Gantt panel in the lower workspace that plays a construction-sequence animation driven by IfcTask dates and the products each task controls.
- Creating a schedule from scratch:
  \`\`\`js
  const h = bim.create.project({ Name: "Demo" });
  const storey = bim.create.addIfcBuildingStorey(h, { Name: "Ground", Elevation: 0 });
  const wallA = bim.create.addIfcWall(h, storey, { Start: [0,0,0], End: [5,0,0], Thickness: 0.2, Height: 3 });

  const schedule = bim.create.addIfcWorkSchedule(h, {
    Name: "Construction schedule",
    StartTime: "2024-05-01T08:00:00",
    FinishTime: "2024-06-30T17:00:00",
    PredefinedType: "PLANNED",
  });
  const task = bim.create.addIfcTask(h, {
    Name: "Install wall A",
    PredefinedType: "INSTALLATION",
    ScheduleStart: "2024-05-06T08:00:00",
    ScheduleFinish: "2024-05-10T17:00:00",
    ScheduleDuration: "P5D",
  });
  bim.create.assignTasksToWorkSchedule(h, schedule, [task]);
  bim.create.assignProductsToTask(h, task, [wallA]);   // products reveal in the 4D animation
  // bim.create.addIfcRelSequence(h, prevTask, task, { SequenceType: "FINISH_START", TimeLag: "P2D" });
  // bim.create.nestTasks(h, summaryTask, [task]);    // WBS hierarchy
  \`\`\`
- Dates are ISO 8601 (\`2024-05-01T08:00:00\`). Durations are ISO 8601 (\`P5D\`, \`PT8H\`).
- IfcTask.PredefinedType is an enum — prefer CONSTRUCTION, INSTALLATION, DEMOLITION, RENOVATION over free strings.
- For milestones (e.g. "handover"), set \`IsMilestone: true\` and omit or equate start/finish.
- \`assignProductsToTask\` is the bridge that lets the 4D Gantt animation reveal/hide elements as time advances. Always bind tasks to the elements they construct when the user wants the viewport to animate.
- Reading an existing schedule: \`bim.schedule.data()\` returns { workSchedules, tasks, sequences }. Use it to inspect or validate a construction plan.
- **CSV / Excel / PDF → schedule workflow:** when the user attaches a spreadsheet or PDF with activities and dates, parse it with \`bim.files.csv(name)\` (for CSV) or \`bim.files.text(name)\` (for text-extracted PDF/Excel content converted upstream). Map each row to an \`addIfcTask(...)\` call and resolve the \`products\` column — an IFC type like \`IfcWall\` expands to every matching entity, a globalId maps to one specific entity — into \`expressId\`s to feed \`assignProductsToTask\`. The \`Construction schedule (4D)\` script template is a ready-made starting point.

## API REFERENCE
${apiRef}

## ENTITY SHAPE
Entities returned by bim.query have this shape:
\`\`\`ts
{ ref: { modelId: string, expressId: number }, GlobalId: string, Name: string, Type: string, Description: string, ObjectType: string }
\`\`\`
Prefer PascalCase as the primary contract. camelCase aliases may exist for compatibility: \`entity.name\`, \`entity.type\`, \`entity.globalId\`.

## PROPERTY SET SHAPE
\`bim.query.properties(entity)\` returns an array of property sets. Prefer PascalCase:
\`\`\`ts
// Each property set:
{ Name: string, Properties: PropertyData[], name?: string, properties?: PropertyData[] }
// Each property:
{ Name: string, NominalValue: ..., name?: string, value?: string|number|boolean|null, Value?: ... }
\`\`\`
Example:
\`\`\`js
const props = bim.query.properties(entity);
for (const pset of props) {
  for (const p of pset.Properties) {
    console.log(pset.Name, p.Name, p.NominalValue);
  }
}
\`\`\`

## IFC METADATA ACCESS
- Materials are usually NOT ordinary property-set values. Prefer \`bim.query.materials(entity)\` over guessing \`Pset_*\` names like \`Pset_MaterialCommon\`.
- Classifications are usually relationship-based references. Prefer \`bim.query.classifications(entity)\` over guessing ad-hoc classification properties.
- Type-driven metadata may live on the type object rather than the occurrence. Use \`bim.query.typeProperties(entity)\` when instance property sets are missing the expected data.
- Documents and relationship-driven metadata are available via \`bim.query.documents(entity)\` and \`bim.query.relationships(entity)\`.
- For general IFC introspection, use \`bim.query.attributes(entity)\` to inspect named IFC attributes on the occurrence itself.
Example:
\`\`\`js
const walls = bim.query.byType("IfcWall", "IfcWallStandardCase");
for (const wall of walls) {
  const material = bim.query.materials(wall);
  const classes = bim.query.classifications(wall);
  console.log(wall.Name, material?.name ?? material?.materials?.join(", ") ?? "No material", classes.map((c) => c.identification ?? c.name).join(", "));
}
\`\`\`

## COLOR NAMES FOR bim.create.setColor
Use RGB arrays [r, g, b] with values 0-1, e.g. [0.8, 0.2, 0.2] for red.

## EXAMPLES

### Create a simple house
\`\`\`js
const h = bim.create.project({ Name: "Simple House" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Ground Floor", Elevation: 0 });
console.log("Created project and storey");

// Walls (10m x 8m footprint, 3m height, 0.25m thick) — Z=0 relative to storey
const northWall = bim.create.addIfcWall(h, s0, {
  Name: "North Wall",
  Start: [0,0,0],
  End: [10,0,0],
  Height: 3,
  Thickness: 0.25,
});
// Use wall-hosted helpers for aligned inserts in the wall's local coordinates.
bim.create.addIfcWallWindow(h, northWall, { Name: "North Window Left", Position: [2.0, 0, 1.0], Width: 1.2, Height: 1.2 });
bim.create.addIfcWallWindow(h, northWall, { Name: "North Window Right", Position: [8.0, 0, 1.0], Width: 1.2, Height: 1.2 });
bim.create.addIfcWall(h, s0, { Name: "East Wall", Start: [10,0,0], End: [10,8,0], Height: 3, Thickness: 0.25 });
bim.create.addIfcWall(h, s0, { Name: "South Wall", Start: [10,8,0], End: [0,8,0], Height: 3, Thickness: 0.25 });
bim.create.addIfcWall(h, s0, { Name: "West Wall", Start: [0,8,0], End: [0,0,0], Height: 3, Thickness: 0.25 });
console.log("Added 4 walls");

// Floor slab — Position is min corner
bim.create.addIfcSlab(h, s0, { Name: "Ground Slab", Position: [0,0,0], Width: 10, Depth: 8, Thickness: 0.3 });

// Gable roof at Z=3 (top of walls, still relative to storey) — slope must be radians
bim.create.addIfcGableRoof(h, s0, { Name: "Main Roof", Position: [0,0,3], Width: 10, Depth: 8, Thickness: 0.2, Slope: Math.PI / 12, Overhang: 0.3 });
console.log("Added slab and roof");

const result = bim.create.toIfc(h);
bim.model.loadIfc(result.content, "simple-house.ifc");
console.log("Created house with", result.stats.entityCount, "entities");
\`\`\`

### Minimal API contract (strict keys and arg order)
\`\`\`js
const h = bim.create.project({ Name: "Contract Example" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
const wall = bim.create.addIfcWall(h, s0, {
  Name: "W1",
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.25,
  Height: 3,
});
const slab = bim.create.addIfcSlab(h, s0, {
  Name: "S1",
  Position: [0, 0, 0],
  Width: 5,
  Depth: 4,
  Thickness: 0.3,
});
bim.create.setColor(h, wall, "Wall Grey", [0.7, 0.7, 0.7]);
bim.create.setColor(h, slab, "Slab Grey", [0.6, 0.6, 0.6]);
const result = bim.create.toIfc(h);
bim.model.loadIfc(result.content, "contract.ifc");
console.log("Created", result.stats.entityCount, "entities");
\`\`\`

### Multi-storey building (storey-relative methods only)
\`\`\`js
const h = bim.create.project({ Name: "Office" });
for (let i = 0; i < 5; i++) {
  const elev = i * 3.5;
  const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level " + i, Elevation: elev });
  // These methods are storey-relative, so Z=0 means floor level of this storey
  bim.create.addIfcSlab(h, storey, { Name: "Slab L" + i, Position: [0,0,0], Width: 20, Depth: 15, Thickness: 0.3 });
  bim.create.addIfcWall(h, storey, { Name: "Wall L" + i, Start: [0,0,0], End: [20,0,0], Height: 3.5, Thickness: 0.25 });
  console.log("Created Level", i, "at", elev, "m");
}
const result = bim.create.toIfc(h);
bim.model.loadIfc(result.content, "office.ifc");
console.log("Created", result.stats.entityCount, "entities");
\`\`\`

### Colorize walls
\`\`\`js
const walls = bim.query.byType("IfcWall");
console.log("Found", walls.length, "walls");
bim.viewer.colorize(walls, "#3399ee");
console.log("Colored", walls.length, "walls blue");
\`\`\`

### Query and export data
\`\`\`js
const slabs = bim.query.byType("IfcSlab");
console.log("Found", slabs.length, "slabs");
const csv = bim.export.csv(slabs, { columns: ["Name", "Type", "GlobalId"] });
bim.export.download(csv, "slabs.csv", "text/csv");
console.log("Exported CSV with", slabs.length, "rows");
\`\`\`

### Type-level edit
\`\`\`js
const wall = bim.query.selection()[0];
const typeProps = bim.query.typeProperties(wall);
if (wall && typeProps) {
  bim.mutate.setProperty(typeProps.type, "Pset_WallCommon", "Reference", "EXT");
}
\`\`\``;

  prompt += `

### Attachment-driven model edit (common workflow)
\`\`\`js
const rows = bim.files.csv("entities.csv");
if (!rows) {
  console.log("CSV not found");
} else {
  const byGuid = {};
  for (const row of rows) {
    const guid = row.GlobalId || row.globalId;
    const desc = row.Description ?? row.description ?? "";
    if (guid) byGuid[guid] = desc;
  }

  const entities = bim.query.all();
  let updated = 0;
  for (const entity of entities) {
    const guid = entity.GlobalId || entity.globalId;
    if (!guid || !Object.prototype.hasOwnProperty.call(byGuid, guid)) continue;
    bim.mutate.setAttribute(entity, "Description", byGuid[guid]);
    updated++;
  }

  console.log("Updated", updated, "entities");

  // Export only if the user asked for a file.
  // Otherwise stop here and let the user inspect the viewer state.
  // bim.export.ifc(entities, { filename: "updated.ifc", includeMutations: true });
}
\`\`\``;

  // Inject current model context
  if (modelContext) {
    prompt += `\n\n## CURRENT MODEL STATE`;

    if (modelContext.models.length > 0) {
      prompt += `\nLoaded models: ${modelContext.models.map((m) => `${m.name} (${m.entityCount} entities)`).join(', ')}`;
    } else {
      prompt += `\nNo models loaded — the user may want to create one from scratch.`;
    }

    if (Object.keys(modelContext.typeCounts).length > 0) {
      const top = Object.entries(modelContext.typeCounts)
        .sort(([, a], [, b]) => b - a)
        .slice(0, 10);
      prompt += `\nEntity types: ${top.map(([type, count]) => `${type}: ${count}`).join(', ')}`;
    }

    if (modelContext.storeys && modelContext.storeys.length > 0) {
      const topStoreys = modelContext.storeys.slice(0, 12);
      prompt += `\nStoreys: ${topStoreys.map((storey) => {
        const prefix = storey.modelName ? `${storey.modelName}: ` : '';
        const height = storey.height !== undefined ? `, height≈${storey.height}m` : '';
        const elements = storey.elementCount !== undefined ? `, elements=${storey.elementCount}` : '';
        return `${prefix}${storey.name} @ ${storey.elevation}m${height}${elements}`;
      }).join(' | ')}`;
      if (modelContext.storeys.length > topStoreys.length) {
        prompt += `\nAdditional storeys omitted: ${modelContext.storeys.length - topStoreys.length}.`;
      }
    }

    if (modelContext.selectedCount > 0) {
      prompt += `\n${modelContext.selectedCount} entities currently selected in the viewer.`;
    }

    if (modelContext.selectedEntities && modelContext.selectedEntities.length > 0) {
      prompt += `\nSelected entities: ${modelContext.selectedEntities.map((entity) => {
        const prefix = entity.modelName ? `${entity.modelName}: ` : '';
        const kind = entity.selectionKind ? `, kind=${entity.selectionKind}` : '';
        const storey = entity.storeyName ? `, storey=${entity.storeyName}${entity.storeyElevation !== undefined ? `@${entity.storeyElevation}m` : ''}` : '';
        const psets = entity.propertySets && entity.propertySets.length > 0 ? `, psets=${entity.propertySets.join('/')}` : '';
        const typePsets = entity.typePropertySets && entity.typePropertySets.length > 0 ? `, typePsets=${entity.typePropertySets.join('/')}` : '';
        const qsets = entity.quantitySets && entity.quantitySets.length > 0 ? `, qsets=${entity.quantitySets.join('/')}` : '';
        const material = entity.materialName ? `, material=${entity.materialName}` : '';
        const classifications = entity.classifications && entity.classifications.length > 0 ? `, classifications=${entity.classifications.join('/')}` : '';
        return `${prefix}${entity.type} "${entity.name}"${kind}${storey}${psets}${typePsets}${qsets}${material}${classifications}`;
      }).join(' | ')}`;
    }
  }

  // Inject file attachment context
  if (attachments && attachments.length > 0) {
    prompt += `\n\n## UPLOADED FILES`;
    for (const file of attachments) {
      prompt += `\n- ${file.name} (${file.type}, ${(file.size / 1024).toFixed(1)} KB)`;
      if (file.csvColumns) {
        prompt += `\n  Columns: ${file.csvColumns.join(', ')}`;
        prompt += `\n  Rows: ${file.csvData?.length ?? 'unknown'}`;
        if (file.csvData && file.csvData.length > 0) {
          prompt += `\n  Sample (first ${Math.min(MAX_ATTACHMENT_ROWS_IN_PROMPT, file.csvData.length)} rows): ${JSON.stringify(file.csvData.slice(0, MAX_ATTACHMENT_ROWS_IN_PROMPT))}`;
        }
      }
      if (file.textContent) {
        const preview = file.textContent.slice(0, MAX_ATTACHMENT_TEXT_PREVIEW_CHARS);
        prompt += `\n  Text preview: ${JSON.stringify(preview)}`;
        if (file.textContent.length > preview.length) {
          prompt += `\n  Text preview truncated from ${file.textContent.length} chars.`;
        }
      }
    }
    prompt += '\nUploaded files remain available to scripts at runtime via `bim.files.list()`, `bim.files.text(name)`, `bim.files.csv(name)`, and `bim.files.csvColumns(name)`.';
    prompt += '\nThe prompt only shows a preview/sample. Scripts can access the full attachment contents at runtime.';
    prompt += '\nThe sandbox does not provide browser globals like `fetch()`. Do not use `fetch()` for chat attachments.';
  }

  if (scriptEditor) {
    prompt += `\n\n## SCRIPT EDITOR CONTEXT`;
    prompt += `\nCurrent script revision: ${scriptEditor.revision}`;
    prompt += `\nCurrent selection: from=${scriptEditor.selection.from}, to=${scriptEditor.selection.to}`;
    prompt += `\nCurrent script content:\n\`\`\`js\n${scriptEditor.content}\n\`\`\``;
  }

  if (task?.diagnostics && task.diagnostics.length > 0) {
    prompt += `\n\n## ACTIVE DIAGNOSTICS`;
    prompt += `\n${formatDiagnosticsForPrompt(task.diagnostics)}`;
  }

  if (task?.includeAuthoringContract) {
    // AI extension authoring contract (RFC §04.5/§11). Deterministic
    // for a given SDK version so a hosted cache layer hits cleanly.
    // Inject the live SDK version so the AI emits a compatible
    // `engines.ifcLiteSdk` range instead of guessing a future major.
    const sdkVersion = typeof __APP_VERSION__ === 'string' && __APP_VERSION__.length > 0
      ? __APP_VERSION__
      : undefined;
    prompt += `\n\n${buildAuthoringContract({ currentSdkVersion: sdkVersion })}`;
  }

  if (task?.personalOverlay && task.personalOverlay.trim().length > 0) {
    // Personal prompt overlay — durable user preferences captured by
    // the memory loop (RFC §06.4). Cached separately so the rest of
    // the prompt stays cacheable across users.
    prompt += `\n\n## PERSONAL CONTEXT (from the user's flavor)`;
    prompt += `\n${task.personalOverlay.trim()}`;
  }

  return prompt;
}
