/**
 * CBS message schema plugin: extracts CAANSchemaInfo patterns.
 */

import { readFileSync } from "node:fs";
import { makeNodeId } from "../graph/model";
import { getNodeText, type TSNode, type TSTree } from "../scanner/parser";
import { EdgeType, type GraphEdge, type GraphNode, NodeType, type SourceLocation } from "../types";
import type { PatternPlugin } from "./base";

export class CBSSchemaPlugin implements PatternPlugin {
  readonly name = "cbs_schema";

  matches(tree: TSTree, filePath: string): boolean {
    return findSchemaClass(tree.rootNode, filePath) !== undefined;
  }

  extract(tree: TSTree, filePath: string): (GraphNode | GraphEdge)[] {
    let source: string;
    try {
      source = readFileSync(filePath, "utf-8");
    } catch {
      return [];
    }

    const results: (GraphNode | GraphEdge)[] = [];

    for (const classNode of tree.rootNode.children) {
      if (classNode.type !== "class_declaration") continue;
      if (!extendsSchemaInfo(classNode, source)) continue;

      const className = getIdentifier(classNode, source);
      if (!className) continue;

      const fields = extractContentsArray(classNode, source);
      if (fields.length === 0) continue;

      const schemaId = makeNodeId(filePath, className, NodeType.CBS_SCHEMA);
      const location: SourceLocation = {
        filePath,
        lineStart: classNode.startPosition.row + 1,
        lineEnd: classNode.endPosition.row + 1,
      };

      results.push({
        id: schemaId,
        type: NodeType.CBS_SCHEMA,
        name: className,
        location,
        attributes: {
          fields,
          field_count: fields.length,
          has_list_reference: fields.some(
            (f) => (f.type ?? "").includes("[]"),
          ),
        },
      });

      // Link schema to the class node
      const classId = makeNodeId(filePath, className, NodeType.CLASS);
      results.push({
        source: classId,
        target: schemaId,
        type: EdgeType.CONTAINS,
        attributes: { relationship: "defines_schema" },
      });
    }

    return results;
  }
}

function findSchemaClass(root: TSNode, filePath: string): TSNode | undefined {
  let source: string;
  try {
    source = readFileSync(filePath, "utf-8");
  } catch {
    return undefined;
  }
  for (const child of root.children) {
    if (child.type === "class_declaration" && extendsSchemaInfo(child, source)) {
      return child;
    }
  }
  return undefined;
}

function extendsSchemaInfo(classNode: TSNode, source: string): boolean {
  for (const child of classNode.children) {
    if (child.type === "superclass") {
      const text = getNodeText(child, source);
      return text.includes("CAANSchemaInfo");
    }
  }
  return false;
}

function getIdentifier(node: TSNode, source: string): string {
  for (const child of node.children) {
    if (child.type === "identifier") {
      return getNodeText(child, source);
    }
  }
  return "";
}

function extractContentsArray(
  classNode: TSNode,
  source: string,
): { name: string; type: string }[] {
  const fields: { name: string; type: string }[] = [];

  let body: TSNode | undefined;
  for (const child of classNode.children) {
    if (child.type === "class_body") {
      body = child;
      break;
    }
  }
  if (!body) return fields;

  for (const member of body.children) {
    if (member.type !== "field_declaration") continue;
    const text = getNodeText(member, source);
    if (!text.includes("CONTENTS")) continue;
    extractFieldsFromNode(member, source, fields);
    break;
  }

  return fields;
}

function extractFieldsFromNode(
  node: TSNode,
  source: string,
  fields: { name: string; type: string }[],
): void {
  for (const child of node.children) {
    if (child.type === "variable_declarator") {
      for (const sub of child.children) {
        if (sub.type === "array_initializer") {
          extractFromArrayInit(sub, source, fields);
        }
      }
    } else if (child.type === "array_initializer") {
      extractFromArrayInit(child, source, fields);
    } else {
      extractFieldsFromNode(child, source, fields);
    }
  }
}

function extractFromArrayInit(
  arrayInit: TSNode,
  source: string,
  fields: { name: string; type: string }[],
): void {
  for (const child of arrayInit.children) {
    if (child.type === "array_initializer") {
      const strings: string[] = [];
      for (const item of child.children) {
        if (item.type === "string_literal") {
          const text = getNodeText(item, source);
          strings.push(text.replace(/^"|"$/g, ""));
        }
      }
      if (strings.length >= 2) {
        fields.push({ name: strings[0], type: strings[1] });
      }
    }
  }
}
