import type { GraphNode, ProvenanceInfo } from './types.js';
import type { GraphModel } from './graph-model.js';

/**
 * Known provenance-related predicate URIs.
 * These are checked across multiple namespace variants.
 */
const PROV_PREDICATES = {
  wasDerivedFrom: [
    'http://www.w3.org/ns/prov#wasDerivedFrom',
  ],
  wasGeneratedBy: [
    'http://www.w3.org/ns/prov#wasGeneratedBy',
  ],
  generatedAtTime: [
    'http://www.w3.org/ns/prov#generatedAtTime',
  ],
  contentHash: [
    'https://umanitek.ai/dkg/vocab/contentHash',
    'https://guardiankg.org/vocab/contentHash',
    'http://umanitek.ai/dkg/vocab/contentHash',
  ],
  dataSource: [
    'https://umanitek.ai/dkg/vocab/dataSource',
    'https://guardiankg.org/vocab/dataSource',
    'http://umanitek.ai/dkg/vocab/dataSource',
  ],
};

const NAME_PREDICATES = [
  'https://schema.org/name',
  'http://schema.org/name',
  'http://xmlns.com/foaf/0.1/name',
  'http://www.w3.org/2000/01/rdf-schema#label',
];

const DATA_SOURCE_TYPES = [
  'https://umanitek.ai/dkg/vocab/DataSource',
  'http://umanitek.ai/dkg/vocab/DataSource',
  'https://guardiankg.org/vocab/DataSource',
  'http://www.w3.org/ns/prov#Agent',
];

/**
 * Resolves provenance information for graph nodes by traversing
 * metadata properties and following provenance edges.
 *
 * Populates `node.provenance` with:
 * - Data sources (URIs + human-readable names)
 * - Generation timestamp
 * - Generating agent/model
 * - Content hash
 * - UAL and blockchain anchor (when available)
 */
export class ProvenanceResolver {
  /**
   * Resolve provenance for all nodes in the model.
   * Should be called after data is loaded and before rendering.
   */
  resolve(model: GraphModel): void {
    for (const node of model.nodes.values()) {
      node.provenance = this._resolveNodeProvenance(node, model);
    }
  }

  private _resolveNodeProvenance(node: GraphNode, model: GraphModel): ProvenanceInfo {
    const info: ProvenanceInfo = {
      sources: [],
      sourceNames: [],
    };

    // 1. Content hash (from properties or metadata)
    info.contentHash = this._findValue(node, PROV_PREDICATES.contentHash);

    // 2. Generation timestamp
    info.generatedAt = this._findMetadataValue(node, PROV_PREDICATES.generatedAtTime);

    // 3. Generated by (agent/model)
    const generatedByUri = this._findMetadataValue(node, PROV_PREDICATES.wasGeneratedBy);
    if (generatedByUri) {
      info.generatedBy = generatedByUri;
      // Try to resolve the agent's name
      const agentNode = model.nodes.get(generatedByUri);
      if (agentNode) {
        info.generatedByName = this._resolveName(agentNode) ?? undefined;
      } else {
        // Use the last path segment as a fallback name
        info.generatedByName = generatedByUri.split('/').pop() ?? generatedByUri;
      }
    }

    // 4. Data sources — follow wasDerivedFrom edges
    const sourceUris = new Set<string>();
    const sourceNames: string[] = [];

    // Direct edges from this node
    const edges = model.getEdgesFrom(node.id);
    for (const edge of edges) {
      if (PROV_PREDICATES.wasDerivedFrom.some(p => edge.predicate === p)) {
        const targetNode = model.nodes.get(edge.target);
        if (targetNode) {
          // Check if this is a DataSource entity
          const isDataSource = targetNode.types.some(t => DATA_SOURCE_TYPES.includes(t));
          if (isDataSource) {
            if (!sourceUris.has(edge.target)) {
              sourceUris.add(edge.target);
              const name = this._resolveName(targetNode) ?? edge.target.split('/').pop() ?? edge.target;
              sourceNames.push(name);
            }
          }
        }
      }
    }

    // Also check metadata for wasDerivedFrom (may be stored as metadata)
    for (const pred of PROV_PREDICATES.wasDerivedFrom) {
      const vals = node.metadata?.get(pred);
      if (vals) {
        for (const v of vals) {
          const uri = v.value;
          if (sourceUris.has(uri)) continue;
          const targetNode = model.nodes.get(uri);
          if (targetNode) {
            const isDataSource = targetNode.types.some(t => DATA_SOURCE_TYPES.includes(t));
            if (isDataSource) {
              sourceUris.add(uri);
              const name = this._resolveName(targetNode) ?? uri.split('/').pop() ?? uri;
              sourceNames.push(name);
            }
          }
        }
      }
    }

    // Also check for direct dataSource predicates
    for (const pred of PROV_PREDICATES.dataSource) {
      const vals = node.properties?.get(pred) ?? node.metadata?.get(pred);
      if (vals) {
        for (const v of vals) {
          const uri = v.value;
          if (sourceUris.has(uri)) continue;
          sourceUris.add(uri);
          const targetNode = model.nodes.get(uri);
          const name = targetNode ? (this._resolveName(targetNode) ?? uri.split('/').pop() ?? uri) : (uri.split('/').pop() ?? uri);
          sourceNames.push(name);
        }
      }
    }

    info.sources = [...sourceUris];
    info.sourceNames = sourceNames;

    return info;
  }

  /** Find a value from a node's properties, checking multiple predicate URIs */
  private _findValue(node: GraphNode, predicates: string[]): string | undefined {
    for (const pred of predicates) {
      const vals = node.properties?.get(pred);
      if (vals && vals.length > 0) return vals[0].value;
    }
    return undefined;
  }

  /** Find a value from a node's metadata, checking multiple predicate URIs */
  private _findMetadataValue(node: GraphNode, predicates: string[]): string | undefined {
    for (const pred of predicates) {
      const vals = node.metadata?.get(pred);
      if (vals && vals.length > 0) return vals[0].value;
    }
    // Also check properties as fallback
    return this._findValue(node, predicates);
  }

  /** Resolve a human-readable name for a node */
  private _resolveName(node: GraphNode): string | null {
    for (const pred of NAME_PREDICATES) {
      const vals = node.properties?.get(pred);
      if (vals && vals.length > 0) return vals[0].value;
    }
    return null;
  }
}
