import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { DecisionTree } from './decision-tree.entity';
import { DecisionTreeNode } from '../decision-tree-nodes/decision-tree-node.entity';
import { DecisionTreeEdge } from '../decision-tree-edges/decision-tree-edge.entity';
import { CreateDecisionTreeDto } from './dto/create-decision-tree.dto';
import { UpdateDecisionTreeDto } from './dto/update-decision-tree.dto';
import { SaveGraphDto } from './dto/decision-tree-graph.dto';

@Injectable()
export class DecisionTreesService {
  constructor(
    @InjectRepository(DecisionTree)
    private readonly treesRepo: Repository<DecisionTree>,
    @InjectRepository(DecisionTreeNode)
    private readonly nodesRepo: Repository<DecisionTreeNode>,
    @InjectRepository(DecisionTreeEdge)
    private readonly edgesRepo: Repository<DecisionTreeEdge>,
    private readonly dataSource: DataSource,
  ) {}

  create(dto: CreateDecisionTreeDto): Promise<DecisionTree> {
    return this.treesRepo.save(this.treesRepo.create(dto));
  }

  findAll(status?: string): Promise<DecisionTree[]> {
    return this.treesRepo.find({
      where: status ? { status } : {},
      order: { createdAt: 'DESC' },
    });
  }

  async findById(id: number): Promise<DecisionTree | null> {
    const tree = await this.treesRepo.findOne({ where: { id } });
    if (!tree) return null;

    const [nodes, edges] = await Promise.all([
      this.nodesRepo.find({ where: { treeId: id }, order: { orderIndex: 'ASC' } }),
      this.edgesRepo.find({ where: { treeId: id }, order: { orderIndex: 'ASC' } }),
    ]);

    tree.nodes = nodes;
    tree.edges = edges;
    return tree;
  }

  async update(id: number, dto: UpdateDecisionTreeDto): Promise<DecisionTree | null> {
    await this.treesRepo.update(id, dto);
    return this.findById(id);
  }

  async setStatus(id: number, status: string): Promise<DecisionTree | null> {
    const tree = await this.treesRepo.findOne({ where: { id } });
    if (!tree) throw new NotFoundException('Decision tree not found');

    if (status === 'PUBLISHED') await this.assertPublishable(id);

    await this.treesRepo.update(id, { status });
    return this.findById(id);
  }

  async delete(id: number): Promise<boolean> {
    const result = await this.treesRepo.delete(id);
    return (result.affected || 0) > 0;
  }

  /**
   * Replaces the whole graph atomically. Nodes are addressed by client-supplied
   * keys so the admin panel can wire edges before the rows exist.
   */
  async saveGraph(treeId: number, dto: SaveGraphDto): Promise<DecisionTree | null> {
    const tree = await this.treesRepo.findOne({ where: { id: treeId } });
    if (!tree) throw new NotFoundException('Decision tree not found');

    this.validateGraph(dto);

    await this.dataSource.transaction(async (manager) => {
      await manager.delete(DecisionTreeEdge, { treeId });
      await manager.delete(DecisionTreeNode, { treeId });

      const keyToId = new Map<string, number>();

      for (const [index, node] of dto.nodes.entries()) {
        const saved = await manager.save(
          manager.create(DecisionTreeNode, {
            treeId,
            nodeType: node.nodeType,
            payload: { ...(node.payload || {}), key: node.key },
            orderIndex: node.orderIndex ?? index,
          }),
        );
        keyToId.set(node.key, Number(saved.id));
      }

      for (const [index, edge] of dto.edges.entries()) {
        await manager.save(
          manager.create(DecisionTreeEdge, {
            treeId,
            fromNodeId: keyToId.get(edge.fromKey)!,
            toNodeId: keyToId.get(edge.toKey)!,
            label: edge.label,
            condition: edge.condition,
            orderIndex: edge.orderIndex ?? index,
          }),
        );
      }
    });

    return this.findById(treeId);
  }

  /** A tree is only usable if it has exactly one entry point and no dangling edges. */
  private validateGraph(dto: SaveGraphDto): void {
    const keys = new Set<string>();
    for (const node of dto.nodes) {
      if (keys.has(node.key)) {
        throw new BadRequestException(`Duplicate node key: ${node.key}`);
      }
      keys.add(node.key);
    }

    const startNodes = dto.nodes.filter((n) => n.nodeType === 'START');
    if (startNodes.length !== 1) {
      throw new BadRequestException('A decision tree must have exactly one START node');
    }

    for (const edge of dto.edges) {
      if (!keys.has(edge.fromKey)) {
        throw new BadRequestException(`Edge references unknown node: ${edge.fromKey}`);
      }
      if (!keys.has(edge.toKey)) {
        throw new BadRequestException(`Edge references unknown node: ${edge.toKey}`);
      }
    }
  }

  private async assertPublishable(treeId: number): Promise<void> {
    const nodes = await this.nodesRepo.find({ where: { treeId } });
    if (nodes.length === 0) {
      throw new BadRequestException('Cannot publish a decision tree with no nodes');
    }
    if (!nodes.some((n) => n.nodeType === 'START')) {
      throw new BadRequestException('Cannot publish a decision tree without a START node');
    }
  }
}
