import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { apiDelete, apiGet, apiPost, getProjectId } from "../api-client.js";

function modelEditorPath(asset: any) {
  const sourcePath = asset.sourceDocumentId
    ? `/projects/${asset.projectId}/documents/${asset.sourceDocumentId}/download?inline=1`
    : asset.fileNodeId
      ? `/projects/${asset.projectId}/files/${asset.fileNodeId}/download?inline=1`
      : "";
  const params = new URLSearchParams({ embedded: "1", bidwright: "1" });
  if (sourcePath) params.set("url", sourcePath);
  if (asset.fileName) params.set("fileName", asset.fileName);
  return `/model-editor/index.html?${params.toString()}`;
}

function asRecord(value: any): Record<string, any> {
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function asArray(value: any): any[] {
  return Array.isArray(value) ? value : [];
}

function compactValue(value: any, maxLength = 120) {
  if (value == null) return value;
  if (typeof value === "number" || typeof value === "boolean") return value;
  const text = typeof value === "string" ? value : JSON.stringify(value);
  return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
}

function topRecordEntries(value: any, limit = 8) {
  return Object.entries(asRecord(value))
    .map(([key, entry]) => {
      const record = asRecord(entry);
      const count = Number(record.count ?? record.total ?? record.elements ?? record.quantity ?? entry ?? 0);
      return {
        key,
        count: Number.isFinite(count) ? count : undefined,
        sample: Object.keys(record).length > 0 ? compactValue(record.sample ?? record.label ?? record.name ?? record.type) : undefined,
      };
    })
    .sort((a, b) => (b.count ?? 0) - (a.count ?? 0))
    .slice(0, limit);
}

function previewRows(rows: any[], limit = 8) {
  return rows.slice(0, limit).map((row) => {
    const record = asRecord(row);
    return {
      id: record.id ?? record.elementId ?? record.quantityId ?? null,
      name: compactValue(record.name ?? record.description ?? record.label ?? record.type ?? record.class ?? ""),
      class: compactValue(record.class ?? record.category ?? record.kind ?? ""),
      material: compactValue(record.material ?? ""),
      level: compactValue(record.level ?? record.storey ?? ""),
      quantity: record.quantity ?? record.value ?? record.total ?? null,
      unit: record.unit ?? record.uom ?? null,
    };
  });
}

function paginate<T>(rows: T[], input: { limit?: number; offset?: number }, defaultLimit = 100, maxLimit = 500) {
  const offset = Math.max(0, input.offset ?? 0);
  const limit = Math.max(1, Math.min(input.limit ?? defaultLimit, maxLimit));
  const page = rows.slice(offset, offset + limit);
  return { rows: page, offset, limit, total: rows.length, hasMore: offset + page.length < rows.length };
}

function issuePreview(issues: any[], limit = 6) {
  return issues.slice(0, limit).map((issue) => {
    const record = asRecord(issue);
    return {
      severity: record.severity ?? record.level ?? "info",
      code: record.code ?? record.type ?? null,
      message: compactValue(record.message ?? record.description ?? issue, 180),
    };
  });
}

function summarizeModelAsset(asset: any) {
  const manifest = asRecord(asset.manifest);
  const modelIngest = asRecord(manifest.modelIngest);
  const adapter = asRecord(modelIngest.adapter);
  const counts = asRecord(asset._count);
  const bom = asArray(asset.bom);
  const quantities = asArray(asset.quantities);
  const elements = asArray(asset.elements);
  const issues = asArray(asset.issues);
  const manifestCounts = asRecord(manifest.counts);
  const elementCount = counts.elements ?? manifestCounts.elements ?? manifest.elementCount ?? elements.length;
  const quantityCount = counts.quantities ?? manifestCounts.quantities ?? manifest.quantityCount ?? quantities.length;
  const bomCount = bom.length || manifestCounts.bomRows || manifest.bomRowCount || 0;
  const issueCount = counts.issues ?? manifestCounts.issues ?? issues.length;

  return {
    status: asset.status,
    format: asset.format,
    units: asset.units ?? manifest.units ?? null,
    parser: manifest.parser ?? manifest.parserName ?? manifest.generator ?? null,
    ingestAdapter: adapter.adapterId ?? manifest.adapterId ?? null,
    ingestProvider: adapter.provider ?? manifest.provider ?? null,
    ingestCapabilityStatus: adapter.status ?? manifest.adapterStatus ?? null,
    source: manifest.source ?? (asset.sourceDocumentId ? "source_document" : "file_node"),
    counts: {
      elements: elementCount,
      quantities: quantityCount,
      bomRows: bomCount,
      issues: issueCount,
    },
    elementStats: topRecordEntries(asset.elementStats, 10),
    bomPreview: previewRows(bom, 10),
    quantityPreview: previewRows(quantities, 10),
    elementPreview: previewRows(elements, 10),
    issuesPreview: issuePreview(issues, 8),
    agentHints: [
      elementCount ? "Use queryModelElements for filtered object/type/material/level searches." : null,
      quantityCount || bomCount ? "Use extractModelBom for persisted estimating quantities before creating worksheet items." : null,
      Boolean(manifest.editableInBidWrightModelEditor) ? "Open bidwrightEditorPath for visual QA or manual model review." : null,
      adapter.status && adapter.status !== "available" ? `Ingest adapter status is ${adapter.status}; inspect issues before relying on quantities.` : null,
    ].filter(Boolean),
  };
}

function normalizedAsset(asset: any) {
  const manifest = asset.manifest ?? {};
  return {
    id: asset.id,
    projectId: asset.projectId,
    fileName: asset.fileName,
    format: asset.format,
    status: asset.status,
    units: asset.units,
    checksum: asset.checksum,
    ingestCapabilityStatus: asRecord(asRecord(manifest.modelIngest).adapter).status ?? manifest.adapterStatus ?? null,
    ingestAdapter: asRecord(asRecord(manifest.modelIngest).adapter).adapterId ?? manifest.adapterId ?? null,
    source: manifest.source ?? (asset.sourceDocumentId ? "source_document" : "file_node"),
    sourceDocumentId: asset.sourceDocumentId ?? null,
    fileNodeId: asset.fileNodeId ?? null,
    editableInBidWrightModelEditor: Boolean(manifest.editableInBidWrightModelEditor),
    bidwrightEditorPath: modelEditorPath(asset),
    counts: asset._count ?? null,
    agentSummary: summarizeModelAsset(asset),
    updatedAt: asset.updatedAt ?? null,
  };
}

export function registerModelTools(server: McpServer) {
  server.tool(
    "getModelIngestCapabilities",
    `Report BidWright's CAD/BIM/model ingest capability matrix.

Use this before relying on RVT, DWG, IFC, DXF, STEP, mesh, or Autodesk APS extraction. It returns explicit adapter states: available, missing, unsupported, degraded, or failed.`,
    {
      format: z.string().optional().describe("Optional file extension filter, such as ifc, rvt, dwg, dxf, step, stl, obj, or glb."),
    },
    async ({ format }) => {
      const projectId = getProjectId();
      const params = new URLSearchParams();
      if (format) params.set("format", format);
      const result = await apiGet<any>(`/api/models/${projectId}/ingest-capabilities?${params.toString()}`);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    },
  );

  server.tool(
    "listModels",
    `Scan and list model/CAD/BIM assets in the current project.

Use this before model takeoff, model QA, model/BOM inspection, or when choosing a 3D file to open in BidWright's model editor.`,
    {},
    async () => {
      const projectId = getProjectId();
      const result = await apiGet<any>(`/api/models/${projectId}/assets?refresh=1`);
      const assets = result.assets ?? [];
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(
              {
                count: assets.length,
                syncedIds: result.syncedIds ?? [],
                sourceCount: result.sourceCount ?? assets.length,
                models: assets.map(normalizedAsset),
              },
              null,
              2,
            ),
          },
        ],
      };
    },
  );

  server.tool(
    "getModelManifest",
    `Return a persisted BidWright model manifest.

The manifest includes parser status, source lineage, native file metadata, element/quantity counts, model issues, and any extracted model tree data.`,
    {
      modelId: z.string().describe("Model asset id returned by listModels"),
      includeRaw: z.boolean().default(false).describe("Include raw manifest arrays. Default false returns compact previews only."),
      limit: z.coerce.number().int().positive().max(200).default(50).describe("Rows per raw array when includeRaw=true."),
      offset: z.coerce.number().int().min(0).default(0).describe("Offset for raw arrays when includeRaw=true."),
    },
    async ({ modelId, includeRaw, limit, offset }) => {
      const projectId = getProjectId();
      const result = await apiGet<any>(`/api/models/${projectId}/assets/${modelId}`);
      const asset = result.asset;
      const elementsPage = paginate(asArray(asset.elements), { limit, offset }, 50, 200);
      const quantitiesPage = paginate(asArray(asset.quantities), { limit, offset }, 50, 200);
      const bomPage = paginate(asArray(asset.bom), { limit, offset }, 50, 200);
      const issuesPage = paginate(asArray(asset.issues), { limit, offset }, 50, 200);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(
              {
                model: normalizedAsset(asset),
                agentSummary: summarizeModelAsset(asset),
                raw: includeRaw ? {
                  manifest: asset.manifest,
                  elementStats: asset.elementStats,
                  bom: { ...bomPage, rows: bomPage.rows },
                  elements: { ...elementsPage, rows: elementsPage.rows },
                  quantities: { ...quantitiesPage, rows: quantitiesPage.rows },
                  issues: { ...issuesPage, rows: issuesPage.rows },
                } : undefined,
                note: includeRaw
                  ? "Raw arrays are paginated with the same limit/offset. Use queryModelElements or extractModelBom for focused data."
                  : "Compact manifest only. Set includeRaw=true with limit/offset, or use queryModelElements/extractModelBom for focused data.",
              },
              null,
              2,
            ),
          },
        ],
      };
    },
  );

  server.tool(
    "queryModelElements",
    `Query persisted model elements by class/type/material/level/system/name/text.

This tool returns only extracted data. It does not infer or fabricate model elements.`,
    {
      modelId: z.string().describe("Model asset id returned by listModels"),
      query: z
        .object({
          text: z.string().optional(),
          class: z.string().optional(),
          type: z.string().optional(),
          material: z.string().optional(),
          level: z.string().optional(),
          system: z.string().optional(),
          name: z.string().optional(),
          limit: z.coerce.number().min(1).max(500).default(100),
          offset: z.coerce.number().min(0).default(0),
        })
        .default({ limit: 100 }),
    },
    async ({ modelId, query }) => {
      const projectId = getProjectId();
      const params = new URLSearchParams();
      for (const [key, value] of Object.entries(query)) {
        if (value !== undefined && value !== "") params.set(key, String(value));
      }
      const result = await apiGet<any>(`/api/models/${projectId}/assets/${modelId}/elements?${params.toString()}`);
      const elements = result.elements ?? [];
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(
              {
                modelId,
                total: result.count ?? elements.length ?? 0,
                offset: query.offset ?? 0,
                limit: query.limit ?? 100,
                hasMore: (query.offset ?? 0) + elements.length < (result.count ?? elements.length ?? 0),
                elements,
              },
              null,
              2,
            ),
          },
        ],
      };
    },
  );

  server.tool(
    "extractModelBom",
    `Return the persisted model BOM/quantity rows for estimating.

The BOM is conservative: unsupported formats return a clear status and empty rows instead of guessed quantities.`,
    {
      modelId: z.string().describe("Model asset id returned by listModels"),
      grouping: z.string().optional().describe("Requested grouping note, such as material, class, system, level, or assembly"),
      limit: z.coerce.number().int().positive().max(500).default(100),
      offset: z.coerce.number().int().min(0).default(0),
    },
    async ({ modelId, grouping, limit, offset }) => {
      const projectId = getProjectId();
      const result = await apiGet<any>(`/api/models/${projectId}/assets/${modelId}/bom`);
      const rows = result.rows ?? [];
      const page = paginate(rows, { limit, offset }, 100, 500);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(
              {
                model: normalizedAsset(result.model),
                grouping: grouping ?? "native",
                status: rows.length > 0 ? "bom_available" : "no_bom_rows_available",
                rowCount: result.rowCount ?? rows.length,
                offset: page.offset,
                limit: page.limit,
                hasMore: page.hasMore,
                rows: page.rows,
              },
              null,
              2,
            ),
          },
        ],
      };
    },
  );

  server.tool(
    "getModelTakeoffLinks",
    `Return the live 5D links between a model asset and quote worksheet line items.

Use this to understand which model elements/quantities already drive estimate rows before creating, editing, or deleting line items.`,
    {
      modelId: z.string().describe("Model asset id returned by listModels"),
    },
    async ({ modelId }) => {
      const projectId = getProjectId();
      const result = await apiGet<any>(`/api/models/${projectId}/assets/${modelId}/takeoff-links`);
      const links = result.links ?? [];
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(
              {
                modelId,
                count: links.length,
                links,
              },
              null,
              2,
            ),
          },
        ],
      };
    },
  );

  server.tool(
    "linkModelElementToWorksheetItem",
    `Create a 5D takeoff link from a model element or model quantity to an existing worksheet line item.

Call createWorksheetItem first when a new estimate row is needed, then call this tool with the returned worksheetItemId and the modelElementId/modelQuantityId from queryModelElements or getModelManifest.`,
    {
      modelId: z.string().describe("Model asset id returned by listModels"),
      worksheetItemId: z.string().describe("Worksheet item id to connect to model geometry"),
      modelElementId: z.string().optional().describe("Model element id, when the line item comes from a specific object/assembly"),
      modelQuantityId: z.string().optional().describe("Persisted model quantity id, when the line item should track a specific extracted quantity"),
      quantityField: z.string().default("quantity").describe("Worksheet item field driven by the model quantity"),
      multiplier: z.coerce.number().default(1).describe("Multiplier applied to the model quantity before writing/recording derivedQuantity"),
      derivedQuantity: z.coerce.number().optional().describe("Resolved quantity used for the worksheet item if already known"),
      selection: z.record(z.unknown()).optional().describe("Optional UI/model selection payload for traceability"),
    },
    async ({ modelId, ...input }) => {
      const projectId = getProjectId();
      const result = await apiPost<any>(`/api/models/${projectId}/assets/${modelId}/takeoff-links`, input);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(
              {
                created: true,
                modelId,
                link: result.link ?? result,
              },
              null,
              2,
            ),
          },
        ],
      };
    },
  );

  server.tool(
    "deleteModelTakeoffLink",
    "Delete a 5D takeoff link between a model asset and a worksheet line item. This does not delete the worksheet item itself.",
    {
      modelId: z.string().describe("Model asset id returned by listModels"),
      linkId: z.string().describe("Model takeoff link id returned by getModelTakeoffLinks"),
    },
    async ({ modelId, linkId }) => {
      const projectId = getProjectId();
      const result = await apiDelete<any>(`/api/models/${projectId}/assets/${modelId}/takeoff-links/${linkId}`);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(result ?? { deleted: true, linkId }, null, 2),
          },
        ],
      };
    },
  );
}
