import { Injectable } from '@nestjs/common';
import { Prisma } from '@/generated/prisma/client';
import { PrismaService } from '@/prisma/prisma.service';
import type { StakeholderDashboardSummary } from './stakeholder-dashboard-summary.types';

// Stakeholder type used by the UI as "Working Group".
const WORKING_GROUP_TYPE_NAME = 'Private Sector';

import type {
  StakeholderSortField,
  StakeholderSortOrder,
} from './dto/list-stakeholders.dto';

@Injectable()
export class StakeholdersRepository {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Full detail shape returned by stakeholder-type CRUD endpoints.
   */
  private readonly typeDetailSelect = {
    id: true,
    name: true,
    userId: true,
    createdAt: true,
    updatedAt: true,
    deletedAt: true,
  } satisfies Prisma.StakeholderTypesSelect;

  /**
   * Compact stakeholder shape used when nesting a related stakeholder inside
   * another stakeholder's response (avoids deep/recursive selects).
   */
  private readonly relatedStakeholderSelect = {
    id: true,
    name: true,
    stakeholderTypeId: true,
    logo: true,
    description: true,
    coChair: true,
    active: true,
  } satisfies Prisma.StakeholderSelect;

  /**
   * Fields of a user assigned to a stakeholder, included in list/detail
   * responses (the service flattens the join rows into a plain `users` array).
   */
  private readonly assignedUserSelect = {
    id: true,
    email: true,
    name: true,
    position: true,
    avatar: true,
  } satisfies Prisma.UserSelect;

  /**
   * Public stakeholder shape returned by list/detail endpoints.
   *
   * relatedStakeholder is the single stakeholder this row is paired with.
   * The pairing is mutual (one-to-one):
   *   working group <-> its Ministry.
   * So a Ministry response now includes its related working group, and a
   * working group response includes its related Ministry.
   */
  private readonly publicSelect = {
    id: true,
    name: true,
    stakeholderTypeId: true,
    stakeholderType: {
      select: {
        id: true,
        name: true,
      },
    },
    logo: true,
    description: true,
    coChair: true,
    relatedStakeholderId: true,
    relatedStakeholder: {
      select: this.relatedStakeholderSelect,
    },
    active: true,
    createdAt: true,
    updatedAt: true,
  } satisfies Prisma.StakeholderSelect;

  /**
   * Stakeholder fields returned inside grouped type results.
   */
  private readonly groupedStakeholderSelect = {
    id: true,
    name: true,
    stakeholderTypeId: true,
    logo: true,
    description: true,
    coChair: true,
    relatedStakeholderId: true,
    active: true,
    createdAt: true,
    updatedAt: true,
  } satisfies Prisma.StakeholderSelect;

  /**
   * Find many stakeholders.
   */
  findMany(filter: {
    page: number;
    limit: number;
    sortBy: StakeholderSortField;
    sortOrder: StakeholderSortOrder;
    stakeholderTypeId?: number;
    stakeholderTypeIds?: number[];
    active?: boolean;
    id?: number;
    search?: string;
    name?: string;
    coChair?: string;
    stakeholderTypeName?: string;
    description?: string;
    years?: number[];
    hasUsers?: boolean;
  }) {
    const skip = (filter.page - 1) * filter.limit;
    const where: Prisma.StakeholderWhereInput = {
      deletedAt: null,
    };

    if (filter.id !== undefined) {
      where.id = filter.id;
    }

    if (filter.stakeholderTypeId !== undefined) {
      where.stakeholderTypeId = filter.stakeholderTypeId;
    }

    if (
      filter.stakeholderTypeIds !== undefined &&
      filter.stakeholderTypeIds.length > 0
    ) {
      where.stakeholderTypeId = {
        in: filter.stakeholderTypeIds,
      };
    }

    if (filter.active !== undefined) {
      where.active = filter.active;
    }

    if (filter.years !== undefined && filter.years.length > 0) {
      where.AND = [
        ...(Array.isArray(where.AND)
          ? where.AND
          : where.AND
            ? [where.AND]
            : []),
        {
          OR: filter.years.map((year) => ({
            createdAt: {
              gte: new Date(`${year}-01-01T00:00:00.000Z`),
              lt: new Date(`${year + 1}-01-01T00:00:00.000Z`),
            },
          })),
        },
      ];
    }

    if (filter.hasUsers === true) {
      where.users = {
        some: {},
      };
    } else if (filter.hasUsers === false) {
      where.users = {
        none: {},
      };
    }

    if (filter.name) {
      where.name = {
        contains: filter.name,
        mode: 'insensitive',
      };
    }

    if (filter.coChair) {
      where.coChair = {
        contains: filter.coChair,
        mode: 'insensitive',
      };
    }

    if (filter.description) {
      where.description = {
        contains: filter.description,
        mode: 'insensitive',
      };
    }

    if (filter.stakeholderTypeName) {
      where.stakeholderType = {
        name: {
          contains: filter.stakeholderTypeName,
          mode: 'insensitive',
        },
      };
    }

    if (filter.search) {
      where.OR = [
        {
          name: {
            contains: filter.search,
            mode: 'insensitive',
          },
        },
        {
          description: {
            contains: filter.search,
            mode: 'insensitive',
          },
        },
        {
          coChair: {
            contains: filter.search,
            mode: 'insensitive',
          },
        },
      ];
    }

    const orderBy = this.buildOrderBy(filter.sortBy, filter.sortOrder);

    return this.prisma.$transaction(async (transaction) => {
      const [data, total] = await Promise.all([
        transaction.stakeholder.findMany({
          where,
          orderBy,
          skip,
          take: filter.limit,
          select: {
            ...this.publicSelect,
            users: {
              select: {
                user: { select: this.assignedUserSelect },
              },
            },
          },
        }),
        transaction.stakeholder.count({
          where,
        }),
      ]);

      return {
        data,
        total,
      };
    });
  }

  private buildOrderBy(
    sortBy: StakeholderSortField,
    sortOrder: StakeholderSortOrder,
  ): Prisma.StakeholderOrderByWithRelationInput {
    if (sortBy === 'stakeholderType') {
      return {
        stakeholderType: {
          name: sortOrder,
        },
      };
    }

    return {
      [sortBy]: sortOrder,
    };
  }

  /**
   * List stakeholder types for dropdowns.
   */
  listTypes() {
    return this.prisma.stakeholderTypes.findMany({
      where: {
        deletedAt: null,
      },
      orderBy: {
        name: 'asc',
      },
      select: {
        id: true,
        name: true,
      },
    });
  }

  /**
   * List stakeholder types with their stakeholders.
   */
  listGroupedByType() {
    return this.prisma.stakeholderTypes.findMany({
      where: {
        deletedAt: null,
      },
      orderBy: {
        name: 'asc',
      },
      select: {
        id: true,
        name: true,
        stakeholders: {
          where: {
            deletedAt: null,
          },
          orderBy: {
            name: 'asc',
          },
          select: this.groupedStakeholderSelect,
        },
      },
    });
  }

  /**
   * List active Private Sector stakeholders for Working Group dropdowns.
   */
  listWorkingGroups() {
    return this.prisma.stakeholder.findMany({
      where: {
        active: true,
        deletedAt: null,
        stakeholderType: {
          deletedAt: null,
          name: {
            equals: WORKING_GROUP_TYPE_NAME,
            mode: 'insensitive',
          },
        },
      },
      orderBy: {
        name: 'asc',
      },
      select: this.groupedStakeholderSelect,
    });
  }

  /**
   * Return global stakeholder counts for the admin dashboard.
   */
  getDashboardSummary(): Promise<StakeholderDashboardSummary> {
    return this.prisma.$transaction(async (transaction) => {
      const activeStakeholderWhere: Prisma.StakeholderWhereInput = {
        deletedAt: null,
        stakeholderType: {
          deletedAt: null,
        },
      };

      const [
        totalStakeholders,
        activeStakeholders,
        stakeholderTypes,
        stakeholderTypeStatusCounts,
        assignedUsers,
      ] = await Promise.all([
        transaction.stakeholder.count({
          where: activeStakeholderWhere,
        }),
        transaction.stakeholder.count({
          where: {
            ...activeStakeholderWhere,
            active: true,
          },
        }),
        transaction.stakeholderTypes.findMany({
          where: {
            deletedAt: null,
          },
          orderBy: {
            name: 'asc',
          },
          select: {
            id: true,
            name: true,
          },
        }),
        transaction.stakeholder.groupBy({
          by: ['stakeholderTypeId', 'active'],
          where: activeStakeholderWhere,
          _count: {
            _all: true,
          },
        }),
        transaction.stakeholderUser.findMany({
          where: {
            stakeholder: activeStakeholderWhere,
            user: {
              isActive: true,
              deletedAt: null,
            },
          },
          distinct: ['userId'],
          select: {
            userId: true,
          },
        }),
      ]);

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

      for (const statusCount of stakeholderTypeStatusCounts) {
        const key = `${statusCount.stakeholderTypeId}:${statusCount.active}`;
        countByTypeAndStatus.set(key, statusCount._count._all);
      }

      const stakeholderTypeSummaries = stakeholderTypes.map(
        (stakeholderType) => {
          const activeCount =
            countByTypeAndStatus.get(`${stakeholderType.id}:true`) ?? 0;
          const inactiveCount =
            countByTypeAndStatus.get(`${stakeholderType.id}:false`) ?? 0;

          return {
            id: stakeholderType.id,
            name: stakeholderType.name,
            stakeholderCount: activeCount + inactiveCount,
            activeStakeholderCount: activeCount,
            inactiveStakeholderCount: inactiveCount,
          };
        },
      );

      return {
        totalStakeholders,
        activeStakeholders,
        inactiveStakeholders: totalStakeholders - activeStakeholders,
        totalStakeholderTypes: stakeholderTypes.length,
        totalAssignedUsers: assignedUsers.length,
        stakeholderTypes: stakeholderTypeSummaries,
      };
    });
  }

  /**
   * Find one stakeholder type by ID.
   */
  findTypeById(id: number) {
    return this.prisma.stakeholderTypes.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      select: {
        id: true,
      },
    });
  }

  /**
   * Find one stakeholder by ID.
   */
  findById(id: number) {
    return this.prisma.stakeholder.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      select: this.publicSelect,
    });
  }

  /**
   * Find one stakeholder by ID with assigned users.
   */
  findByIdWithUsers(id: number) {
    return this.prisma.stakeholder.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      select: {
        ...this.publicSelect,
        users: {
          select: {
            user: { select: this.assignedUserSelect },
          },
        },
      },
    });
  }

  /**
   * Create stakeholder.
   *
   * If relatedStakeholderId is provided, create a mutual relation:
   * current stakeholder -> related stakeholder
   * related stakeholder -> current stakeholder
   */
  create(data: Prisma.StakeholderCreateInput, relatedStakeholderId?: number) {
    return this.prisma.$transaction(async (transaction) => {
      const stakeholder = await transaction.stakeholder.create({
        data,
        select: {
          id: true,
        },
      });

      if (relatedStakeholderId !== undefined) {
        await this.clearRelatedPair(transaction, relatedStakeholderId);

        await transaction.stakeholder.update({
          where: {
            id: stakeholder.id,
          },
          data: {
            relatedStakeholderId,
          },
        });

        await transaction.stakeholder.update({
          where: {
            id: relatedStakeholderId,
          },
          data: {
            relatedStakeholderId: stakeholder.id,
          },
        });
      }

      return transaction.stakeholder.findUniqueOrThrow({
        where: {
          id: stakeholder.id,
        },
        select: this.publicSelect,
      });
    });
  }

  /**
   * Update stakeholder.
   *
   * relatedStakeholderId behavior:
   * - undefined = do not change relation
   * - null = remove relation
   * - number = set mutual relation
   */
  update(
    id: number,
    data: Prisma.StakeholderUpdateInput,
    relatedStakeholderId?: number | null,
  ) {
    return this.prisma.$transaction(async (transaction) => {
      if (relatedStakeholderId !== undefined) {
        await this.clearRelatedPair(transaction, id);

        if (relatedStakeholderId !== null) {
          await this.clearRelatedPair(transaction, relatedStakeholderId);
        }
      }

      await transaction.stakeholder.update({
        where: {
          id,
        },
        data,
      });

      if (relatedStakeholderId !== undefined && relatedStakeholderId !== null) {
        await transaction.stakeholder.update({
          where: {
            id,
          },
          data: {
            relatedStakeholderId,
          },
        });

        await transaction.stakeholder.update({
          where: {
            id: relatedStakeholderId,
          },
          data: {
            relatedStakeholderId: id,
          },
        });
      }

      return transaction.stakeholder.findUniqueOrThrow({
        where: {
          id,
        },
        select: this.publicSelect,
      });
    });
  }

  /**
   * Soft delete stakeholder.
   */
  softDelete(id: number) {
    return this.prisma.$transaction(async (transaction) => {
      await this.clearRelatedPair(transaction, id);

      return transaction.stakeholder.update({
        where: {
          id,
        },
        data: {
          deletedAt: new Date(),
          active: false,
        },
        select: {
          id: true,
        },
      });
    });
  }

  /**
   * Clear both sides of a mutual stakeholder relation.
   */
  private async clearRelatedPair(
    transaction: Prisma.TransactionClient,
    stakeholderId: number,
  ) {
    const stakeholder = await transaction.stakeholder.findUnique({
      where: {
        id: stakeholderId,
      },
      select: {
        relatedStakeholderId: true,
      },
    });

    if (!stakeholder) {
      return;
    }

    await transaction.stakeholder.updateMany({
      where: {
        relatedStakeholderId: stakeholderId,
      },
      data: {
        relatedStakeholderId: null,
      },
    });

    if (stakeholder.relatedStakeholderId) {
      await transaction.stakeholder.update({
        where: {
          id: stakeholder.relatedStakeholderId,
        },
        data: {
          relatedStakeholderId: null,
        },
      });
    }

    await transaction.stakeholder.update({
      where: {
        id: stakeholderId,
      },
      data: {
        relatedStakeholderId: null,
      },
    });
  }

  /**
   * Find one user-stakeholder membership.
   */
  findMembership(stakeholderId: number, userId: number) {
    return this.prisma.stakeholderUser.findUnique({
      where: {
        userId_stakeholderId: {
          userId,
          stakeholderId,
        },
      },
    });
  }

  /**
   * Add user to stakeholder.
   */
  addUser(stakeholderId: number, userId: number) {
    return this.prisma.stakeholderUser.create({
      data: {
        stakeholderId,
        userId,
      },
      select: {
        id: true,
        userId: true,
        stakeholderId: true,
        createdAt: true,
      },
    });
  }

  /**
   * Remove user from stakeholder.
   */
  removeUser(stakeholderId: number, userId: number) {
    return this.prisma.stakeholderUser.delete({
      where: {
        userId_stakeholderId: {
          userId,
          stakeholderId,
        },
      },
      select: {
        id: true,
      },
    });
  }

  /**
   * List users assigned to a stakeholder.
   */
  listMembers(stakeholderId: number) {
    return this.prisma.stakeholderUser.findMany({
      where: {
        stakeholderId,
      },
      orderBy: {
        createdAt: 'asc',
      },
      select: {
        createdAt: true,
        user: {
          select: {
            id: true,
            email: true,
            name: true,
            position: true,
            avatar: true,
          },
        },
      },
    });
  }

  // ---------------------------------------------------------------------------
  // Stakeholder type CRUD
  // ---------------------------------------------------------------------------

  /**
   * Find stakeholder type detail by ID.
   */
  findTypeDetailById(id: number) {
    return this.prisma.stakeholderTypes.findFirst({
      where: {
        id,
        deletedAt: null,
      },
      select: this.typeDetailSelect,
    });
  }

  /**
   * Find stakeholder type by name.
   */
  findTypeByName(name: string, excludeId?: number) {
    return this.prisma.stakeholderTypes.findFirst({
      where: {
        name: {
          equals: name,
          mode: 'insensitive',
        },
        deletedAt: null,
        ...(excludeId !== undefined && {
          id: {
            not: excludeId,
          },
        }),
      },
      select: {
        id: true,
      },
    });
  }

  /**
   * Create stakeholder type.
   */
  createType(name: string, userId: number) {
    return this.prisma.stakeholderTypes.create({
      data: {
        name,
        user: {
          connect: {
            id: userId,
          },
        },
      },
      select: this.typeDetailSelect,
    });
  }

  /**
   * Update stakeholder type.
   */
  updateType(id: number, data: { name?: string }) {
    return this.prisma.stakeholderTypes.update({
      where: {
        id,
      },
      data: {
        ...(data.name !== undefined && {
          name: data.name,
        }),
      },
      select: this.typeDetailSelect,
    });
  }

  /**
   * Soft delete stakeholder type.
   */
  softDeleteType(id: number) {
    return this.prisma.stakeholderTypes.update({
      where: {
        id,
      },
      data: {
        deletedAt: new Date(),
      },
      select: {
        id: true,
      },
    });
  }
}
