import { BadRequestException, Injectable } from '@nestjs/common';
import { RoleRepository } from './repositories/role.repository';
import { RoleHierarchyRepository } from './repositories/role-hierarchy.repository';
import { type UpdateRoleHierarchyDto } from './dto/update-role-hierarchy.dto';

export interface RoleHierarchyNode {
  id: string;
  name: string;
  is_system: boolean;
  parent_role_id: string | null;
}

@Injectable()
export class RoleHierarchyService {
  constructor(
    private readonly roleRepo: RoleRepository,
    private readonly hierarchyRepo: RoleHierarchyRepository,
  ) {}

  /**
   * Set one role's parent. `null` makes it a root. Creates the hierarchy
   * row when the role does not have one yet.
   */
  async setParent(roleId: string, parentRoleId: string | null): Promise<void> {
    const roles = await this.roleRepo.find();
    const roleIds = new Set(roles.map((role) => role.id));
    if (!roleIds.has(roleId)) {
      throw new BadRequestException('Role not found');
    }
    if (parentRoleId && !roleIds.has(parentRoleId)) {
      throw new BadRequestException('Parent role is outside this tenant');
    }
    if (parentRoleId === roleId) {
      throw new BadRequestException('A role cannot report to itself');
    }

    const rows = await this.hierarchyRepo.find();
    const parentOf = new Map(rows.map((row) => [row.role_id, row.parent_role_id]));
    parentOf.set(roleId, parentRoleId);

    const seen = new Set<string>();
    let current: string | null = roleId;
    while (current) {
      if (seen.has(current)) {
        throw new BadRequestException('Role hierarchy cannot contain a cycle');
      }
      seen.add(current);
      current = parentOf.get(current) ?? null;
    }

    const row = rows.find((item) => item.role_id === roleId);
    if (row) {
      if (row.parent_role_id !== parentRoleId) {
        await this.hierarchyRepo.update(row.id, { parent_role_id: parentRoleId });
      }
      return;
    }
    await this.hierarchyRepo.save({ role_id: roleId, parent_role_id: parentRoleId });
  }

  /**
   * Insert a root row when a role is created. No-op if a row already exists.
   */
  async ensureRoot(roleId: string): Promise<void> {
    const existing = await this.hierarchyRepo.findOne({ where: { role_id: roleId } });
    if (existing) return;
    await this.hierarchyRepo.save({ role_id: roleId, parent_role_id: null });
  }

  /**
   * Drop a role from the tree. Its children move up to its parent so
   * the remaining hierarchy stays connected.
   */
  async detach(roleId: string): Promise<void> {
    const row = await this.hierarchyRepo.findOne({ where: { role_id: roleId } });
    if (!row) return;

    const children = await this.hierarchyRepo.find({ where: { parent_role_id: roleId } });
    for (const child of children) {
      await this.hierarchyRepo.update(child.id, { parent_role_id: row.parent_role_id });
    }
    await this.hierarchyRepo.softDelete(row.id);
  }

  /** Direct parent role in the tenant hierarchy, or null if root / unknown. */
  async getParentRoleId(roleId: string): Promise<string | null> {
    const ancestors = await this.getAncestorRoleIds(roleId);
    return ancestors[0] ?? null;
  }

  /**
   * All ancestor roles from immediate parent up to root (closest first).
   * Used for "Reports to" — user may report to anyone in a role above theirs.
   */
  async getAncestorRoleIds(roleId: string): Promise<string[]> {
    const roles = await this.roleRepo.find();
    const roleIds = new Set(roles.map((r) => r.id));
    const rows = await this.hierarchyRepo.find();
    const parentOf = new Map(rows.map((row) => [row.role_id, row.parent_role_id]));
    const ancestors: string[] = [];
    const seen = new Set<string>();
    let current = parentOf.get(roleId) ?? null;
    while (current && roleIds.has(current)) {
      if (seen.has(current)) break;
      seen.add(current);
      ancestors.push(current);
      current = parentOf.get(current) ?? null;
    }
    return ancestors;
  }

  async get(): Promise<RoleHierarchyNode[]> {
    const roles = await this.roleRepo.find({ order: { name: 'ASC' } });
    const rows = await this.hierarchyRepo.find();
    const byRoleId = new Map(rows.map((row) => [row.role_id, row]));

    for (const role of roles) {
      if (!byRoleId.has(role.id)) {
        const created = await this.hierarchyRepo.save({
          role_id: role.id,
          parent_role_id: null,
        });
        byRoleId.set(role.id, created);
      }
    }

    const roleIds = new Set(roles.map((role) => role.id));
    return roles.map((role) => {
      const parentId = byRoleId.get(role.id)?.parent_role_id ?? null;
      return {
        id: role.id,
        name: role.name,
        is_system: role.is_system,
        parent_role_id: parentId && roleIds.has(parentId) ? parentId : null,
      };
    });
  }

  async replace(dto: UpdateRoleHierarchyDto): Promise<{ message: string }> {
    const roles = await this.roleRepo.find();
    const roleIds = new Set(roles.map((role) => role.id));

    if (dto.links.length !== roleIds.size) {
      throw new BadRequestException('Hierarchy must include every role exactly once');
    }

    const parentOf = new Map<string, string | null>();
    for (const link of dto.links) {
      if (!roleIds.has(link.role_id)) {
        throw new BadRequestException('Hierarchy contains a role outside this tenant');
      }
      if (parentOf.has(link.role_id)) {
        throw new BadRequestException('Hierarchy lists a role more than once');
      }
      const parentId = link.parent_role_id ?? null;
      if (parentId && !roleIds.has(parentId)) {
        throw new BadRequestException('Parent role is outside this tenant');
      }
      if (parentId === link.role_id) {
        throw new BadRequestException('A role cannot report to itself');
      }
      parentOf.set(link.role_id, parentId);
    }

    for (const roleId of parentOf.keys()) {
      const seen = new Set<string>();
      let current: string | null = roleId;
      while (current) {
        if (seen.has(current)) {
          throw new BadRequestException('Role hierarchy cannot contain a cycle');
        }
        seen.add(current);
        current = parentOf.get(current) ?? null;
      }
    }

    const existing = await this.hierarchyRepo.find();
    const rowByRole = new Map(existing.map((row) => [row.role_id, row]));

    for (const [roleId, parentId] of parentOf) {
      const row = rowByRole.get(roleId);
      if (row) {
        if (row.parent_role_id !== parentId) {
          await this.hierarchyRepo.update(row.id, { parent_role_id: parentId });
        }
      } else {
        await this.hierarchyRepo.save({ role_id: roleId, parent_role_id: parentId });
      }
    }

    return { message: 'Role hierarchy updated successfully' };
  }
}
