/**
 * Relational resolver for behavioral guidelines.
 *
 * Port of boocontext-audit/src/resolver.ts — resolves DEPENDS_ON,
 * PRIORITIZES, ENTAILS, TAG_ALL, TAG_PRIORITIZES relationships
 * with an iterative convergence loop.
 */

export enum RelationshipKind {
  DEPENDS_ON = 'depends_on',
  PRIORITIZES = 'prioritizes',
  ENTAILS = 'entails',
  TAG_ALL = 'tag_all',
  TAG_PRIORITIZES = 'tag_prioritizes',
}

export enum RelationshipEntityKind {
  GUIDELINE = 'guideline',
  TAG = 'tag',
}

export interface RelationshipEntity {
  id: string;
  kind: RelationshipEntityKind;
}

export interface Relationship {
  id: string;
  creation_utc: string;
  source: RelationshipEntity;
  target: RelationshipEntity;
  kind: RelationshipKind;
  group_id?: string;
}

/**
 * Minimal relationship store interface.
 * The resolver only needs listRelationships. Implementations
 * can back against files, postgres, or in-memory maps.
 */
export interface RelationshipStore {
  listRelationships(
    kind?: RelationshipKind,
    sourceId?: string,
    targetId?: string,
  ): Promise<Relationship[]>;
}

export type ResolvedEntityType = 'guideline' | 'journey' | 'tag';

export interface ResolvedEntity {
  entityType: ResolvedEntityType;
  entityId: string;
}

export enum ResolutionKind {
  NONE = 'none',
  UNMET_DEPENDENCY = 'unmet_dependency',
  DEPRIORITIZED = 'deprioritized',
  ENTAILED = 'entailed',
}

export interface Resolution {
  kind: ResolutionKind;
  description: string;
  relationshipId?: string;
  counterparts?: ResolvedEntity[];
}

export interface GuidelineStub {
  id: string;
  priority: number;
  tags: string[];
}

export interface GuidelineMatchStub {
  guideline: GuidelineStub;
}

export interface ResolverResult {
  matchedIds: Set<string>;
  resolutions: Map<string, Resolution[]>;
  converged: boolean;
  iterations: number;
}

export const MAX_ITERATIONS = 100;

export class RelationalResolver {
  private store: RelationshipStore;

  constructor(store: RelationshipStore) {
    this.store = store;
  }

  async resolve(
    matchedIds: Set<string>,
    allGuidelines: GuidelineStub[],
  ): Promise<ResolverResult> {
    const resolutions = new Map<string, Resolution[]>();
    const guidelinesById = new Map(allGuidelines.map((g) => [g.id, g]));
    let currentIds = new Set(matchedIds);
    const priorityRemoved = new Set<string>();
    const entailedIds = new Set<string>();

    let converged = false;
    let iterations = 0;

    for (iterations = 0; iterations < MAX_ITERATIONS; iterations++) {
      const candidateIds = new Set(
        [...currentIds].filter((id) => !priorityRemoved.has(id)),
      );

      const step1Ids = await this.applyDependencies(candidateIds, guidelinesById, resolutions);

      const step2Ids = await this.applyPrioritization(
        step1Ids,
        guidelinesById,
        resolutions,
        priorityRemoved,
      );

      const step3Ids = this.applyNumericalPriority(
        step2Ids,
        guidelinesById,
        resolutions,
        priorityRemoved,
        entailedIds,
      );

      const step4Ids = await this.applyEntailment(
        step3Ids,
        guidelinesById,
        resolutions,
        priorityRemoved,
        entailedIds,
      );

      if (this.setsEqual(step4Ids, currentIds)) {
        converged = true;
        break;
      }

      currentIds = step4Ids;
    }

    for (const id of allGuidelines.map((g) => g.id)) {
      if (!resolutions.has(id)) {
        resolutions.set(id, [
          { kind: ResolutionKind.NONE, description: 'No relational changes' },
        ]);
      }
    }

    return {
      matchedIds: currentIds,
      resolutions,
      converged,
      iterations: iterations + 1,
    };
  }

  // ── Private steps ──

  private async applyDependencies(
    candidateIds: Set<string>,
    _guidelinesById: Map<string, GuidelineStub>,
    resolutions: Map<string, Resolution[]>,
  ): Promise<Set<string>> {
    const surviving = new Set(candidateIds);
    const cache = new Map<string, Relationship[]>();

    for (const gid of candidateIds) {
      const rels = await this.getRelationshipsFromCache(cache, gid, RelationshipKind.DEPENDS_ON);

      for (const rel of rels) {
        const targetId = rel.target.id;
        if (!candidateIds.has(targetId)) {
          surviving.delete(gid);
          this.addResolution(resolutions, gid, {
            kind: ResolutionKind.UNMET_DEPENDENCY,
            description: `Depends on ${targetId} which is not matched`,
            relationshipId: rel.id,
            counterparts: [{ entityType: 'guideline' as const, entityId: targetId }],
          });
          break;
        }
      }
    }

    return surviving;
  }

  private async applyPrioritization(
    candidateIds: Set<string>,
    guidelinesById: Map<string, GuidelineStub>,
    resolutions: Map<string, Resolution[]>,
    priorityRemoved: Set<string>,
  ): Promise<Set<string>> {
    const surviving = new Set(candidateIds);
    const cache = new Map<string, Relationship[]>();

    for (const gid of candidateIds) {
      if (priorityRemoved.has(gid)) continue;

      const allRels = await this.getAllRelationships(cache, gid);
      const priorityRels = allRels.filter((r) => r.kind === RelationshipKind.PRIORITIZES);

      for (const rel of priorityRels) {
        const sourceId = rel.source.id;
        if (sourceId !== gid) continue;
        const targetId = rel.target.id;

        if (candidateIds.has(targetId)) {
          surviving.delete(targetId);
          priorityRemoved.add(targetId);
          this.addResolution(resolutions, targetId, {
            kind: ResolutionKind.DEPRIORITIZED,
            description: `Deprioritized by ${gid}`,
            relationshipId: rel.id,
            counterparts: [{ entityType: 'guideline' as const, entityId: gid }],
          });
        }
      }
    }

    return surviving;
  }

  private applyNumericalPriority(
    candidateIds: Set<string>,
    guidelinesById: Map<string, GuidelineStub>,
    resolutions: Map<string, Resolution[]>,
    priorityRemoved: Set<string>,
    entailedIds: Set<string>,
  ): Set<string> {
    if (candidateIds.size === 0) return candidateIds;

    const nonEntailed = [...candidateIds].filter((id) => !entailedIds.has(id));
    const entailed = [...candidateIds].filter((id) => entailedIds.has(id));

    if (nonEntailed.length === 0) return new Set(entailed);

    const priorities = nonEntailed.map((id) => guidelinesById.get(id)?.priority ?? 0);
    const maxPriority = Math.max(...priorities);

    const surviving = new Set<string>();

    for (const id of nonEntailed) {
      const priority = guidelinesById.get(id)?.priority ?? 0;
      if (priority >= maxPriority) {
        surviving.add(id);
      } else {
        priorityRemoved.add(id);
        this.addResolution(resolutions, id, {
          kind: ResolutionKind.DEPRIORITIZED,
          description: `Lower priority (${priority} < ${maxPriority})`,
        });
      }
    }

    for (const id of entailed) {
      surviving.add(id);
    }

    return surviving;
  }

  private async applyEntailment(
    candidateIds: Set<string>,
    guidelinesById: Map<string, GuidelineStub>,
    resolutions: Map<string, Resolution[]>,
    priorityRemoved: Set<string>,
    entailedIds: Set<string>,
  ): Promise<Set<string>> {
    const result = new Set(candidateIds);
    const cache = new Map<string, Relationship[]>();

    for (const gid of candidateIds) {
      if (priorityRemoved.has(gid)) continue;

      const allRels = await this.getAllRelationships(cache, gid);
      const entailRels = allRels.filter((r) => r.kind === RelationshipKind.ENTAILS);

      for (const rel of entailRels) {
        const targetId = rel.target.id;
        if (!guidelinesById.has(targetId)) continue;
        if (priorityRemoved.has(targetId)) continue;
        if (entailedIds.has(targetId)) continue;

        result.add(targetId);
        entailedIds.add(targetId);
        this.addResolution(resolutions, targetId, {
          kind: ResolutionKind.ENTAILED,
          description: `Entailed by ${gid}`,
          relationshipId: rel.id,
          counterparts: [{ entityType: 'guideline' as const, entityId: gid }],
        });
      }
    }

    return result;
  }

  // ── Cache helpers ──

  private async getRelationshipsFromCache(
    cache: Map<string, Relationship[]>,
    gid: string,
    kind: RelationshipKind,
  ): Promise<Relationship[]> {
    const key = `${kind}:${gid}`;
    if (!cache.has(key)) {
      cache.set(key, await this.store.listRelationships(kind, gid));
    }
    return cache.get(key)!;
  }

  private async getAllRelationships(
    cache: Map<string, Relationship[]>,
    gid: string,
  ): Promise<Relationship[]> {
    const result: Relationship[] = [];
    const kinds = Object.values(RelationshipKind) as RelationshipKind[];
    for (const kind of kinds) {
      const rels = await this.getRelationshipsFromCache(cache, gid, kind);
      const targetRels = await this.getRelationshipsFromCache(cache, `target:${gid}`, kind);
      result.push(...rels, ...targetRels);
    }
    return result;
  }

  private addResolution(
    resolutions: Map<string, Resolution[]>,
    id: string,
    resolution: Resolution,
  ): void {
    if (!resolutions.has(id)) resolutions.set(id, []);
    resolutions.get(id)!.push(resolution);
  }

  private setsEqual(a: Set<string>, b: Set<string>): boolean {
    if (a.size !== b.size) return false;
    for (const item of a) if (!b.has(item)) return false;
    return true;
  }
}
