import { Repository, IsNull, In } from 'typeorm';
import { Group, User } from '../entities';

export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

export interface CreateGroupDto {
  code: string;
  name: string;
  description?: string;
  parentId?: string;
  memberIds?: string[];
  isActive?: boolean;
}

export interface UpdateGroupDto {
  code?: string;
  name?: string;
  description?: string;
  parentId?: string;
  memberIds?: string[];
  isActive?: boolean;
}

export interface GroupFilters {
  code?: string;
  parentId?: string;
  isActive?: boolean;
  search?: string;
}

export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export class GroupService {
  constructor(
    private readonly groupRepository: Repository<Group>,
    private readonly userRepository: Repository<User>,
  ) {}

  async create(ctx: ServiceContext, data: CreateGroupDto): Promise<Group> {
    const existing = await this.groupRepository.findOne({
      where: { tenantId: ctx.tenantId, code: data.code, deletedAt: IsNull() },
    });

    if (existing) {
      throw new Error(`Group with code '${data.code}' already exists`);
    }

    if (data.parentId) {
      const parent = await this.findById(ctx, data.parentId);
      if (!parent) {
        throw new Error('Parent group not found');
      }
    }

    const group = this.groupRepository.create({
      tenantId: ctx.tenantId,
      code: data.code,
      name: data.name,
      description: data.description,
      parentId: data.parentId,
      isActive: data.isActive ?? true,
      createdBy: ctx.userId ?? null,
    });

    const savedGroup = await this.groupRepository.save(group);

    if (data.memberIds?.length) {
      await this.addMembers(ctx, savedGroup.id, data.memberIds);
    }

    return this.findById(ctx, savedGroup.id) as Promise<Group>;
  }

  async findById(ctx: ServiceContext, id: string): Promise<Group | null> {
    return this.groupRepository.findOne({
      where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
      relations: ['members', 'parent', 'children'],
    });
  }

  async findByCode(ctx: ServiceContext, code: string): Promise<Group | null> {
    return this.groupRepository.findOne({
      where: { tenantId: ctx.tenantId, code, deletedAt: IsNull() },
      relations: ['members', 'parent', 'children'],
    });
  }

  async findAll(ctx: ServiceContext): Promise<Group[]> {
    return this.groupRepository.find({
      where: { tenantId: ctx.tenantId, deletedAt: IsNull() },
      relations: ['members', 'parent'],
      order: { name: 'ASC' },
    });
  }

  async findRootGroups(ctx: ServiceContext): Promise<Group[]> {
    return this.groupRepository.find({
      where: { tenantId: ctx.tenantId, parentId: IsNull(), deletedAt: IsNull() },
      relations: ['children'],
      order: { name: 'ASC' },
    });
  }

  async findWithFilters(
    ctx: ServiceContext,
    filters: GroupFilters,
    page = 1,
    limit = 50,
  ): Promise<PaginatedResult<Group>> {
    const qb = this.groupRepository
      .createQueryBuilder('grp')
      .leftJoinAndSelect('grp.members', 'members')
      .leftJoinAndSelect('grp.parent', 'parent')
      .where('grp.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('grp.deleted_at IS NULL');

    if (filters.code) {
      qb.andWhere('grp.code = :code', { code: filters.code });
    }

    if (filters.parentId) {
      qb.andWhere('grp.parent_id = :parentId', { parentId: filters.parentId });
    }

    if (filters.isActive !== undefined) {
      qb.andWhere('grp.is_active = :isActive', { isActive: filters.isActive });
    }

    if (filters.search) {
      qb.andWhere('(grp.name ILIKE :search OR grp.code ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const [data, total] = await qb
      .orderBy('grp.name', 'ASC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async update(ctx: ServiceContext, id: string, data: UpdateGroupDto): Promise<Group | null> {
    const group = await this.findById(ctx, id);
    if (!group) return null;

    if (data.code && data.code !== group.code) {
      const existing = await this.groupRepository.findOne({
        where: { tenantId: ctx.tenantId, code: data.code, deletedAt: IsNull() },
      });
      if (existing) {
        throw new Error(`Group with code '${data.code}' already exists`);
      }
    }

    if (data.parentId) {
      if (data.parentId === id) {
        throw new Error('A group cannot be its own parent');
      }
      const parent = await this.findById(ctx, data.parentId);
      if (!parent) {
        throw new Error('Parent group not found');
      }
    }

    Object.assign(group, {
      code: data.code ?? group.code,
      name: data.name ?? group.name,
      description: data.description ?? group.description,
      parentId: data.parentId !== undefined ? data.parentId : group.parentId,
      isActive: data.isActive ?? group.isActive,
      updatedBy: ctx.userId ?? null,
    });

    await this.groupRepository.save(group);

    if (data.memberIds !== undefined) {
      await this.syncMembers(ctx, id, data.memberIds);
    }

    return this.findById(ctx, id);
  }

  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const group = await this.findById(ctx, id);
    if (!group) return false;

    group.deletedAt = new Date();
    group.deletedBy = ctx.userId ?? null;
    await this.groupRepository.save(group);

    return true;
  }

  async addMembers(ctx: ServiceContext, groupId: string, userIds: string[]): Promise<void> {
    const group = await this.findById(ctx, groupId);
    if (!group) throw new Error('Group not found');

    const users = await this.userRepository.find({
      where: { id: In(userIds), tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    group.members = [...(group.members || []), ...users];
    await this.groupRepository.save(group);
  }

  async removeMembers(ctx: ServiceContext, groupId: string, userIds: string[]): Promise<void> {
    const group = await this.findById(ctx, groupId);
    if (!group) throw new Error('Group not found');

    group.members = (group.members || []).filter(m => !userIds.includes(m.id));
    await this.groupRepository.save(group);
  }

  async syncMembers(ctx: ServiceContext, groupId: string, userIds: string[]): Promise<void> {
    const group = await this.findById(ctx, groupId);
    if (!group) throw new Error('Group not found');

    const users = await this.userRepository.find({
      where: { id: In(userIds), tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    group.members = users;
    await this.groupRepository.save(group);
  }

  async getGroupHierarchy(ctx: ServiceContext, groupId: string): Promise<Group[]> {
    const hierarchy: Group[] = [];
    let currentId: string | undefined = groupId;

    while (currentId) {
      const group = await this.findById(ctx, currentId);
      if (!group) break;
      hierarchy.unshift(group);
      currentId = group.parentId ?? undefined;
    }

    return hierarchy;
  }

  async getUserGroups(ctx: ServiceContext, userId: string): Promise<Group[]> {
    return this.groupRepository
      .createQueryBuilder('grp')
      .innerJoin('grp.members', 'member')
      .where('grp.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('grp.deleted_at IS NULL')
      .andWhere('member.id = :userId', { userId })
      .getMany();
  }
}
