import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { Prisma, WorkspaceMemberRole } from "@prisma/client";
import type { WorkspaceMember } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import type { AddMemberDto, UpdateMemberRoleDto, WorkspaceResponseDto } from "./dto";

const WORKSPACE_ROLE_RANK: Record<WorkspaceMemberRole, number> = {
  [WorkspaceMemberRole.GUEST]: 1,
  [WorkspaceMemberRole.MEMBER]: 2,
  [WorkspaceMemberRole.ADMIN]: 3,
  [WorkspaceMemberRole.OWNER]: 4,
};

@Injectable()
export class WorkspacesService {
  private readonly logger = new Logger(WorkspacesService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Get all workspaces the user is a member of.
   *
   * Auto-provisioning: if the user has no workspace memberships (e.g. fresh
   * signup via BetterAuth), a default workspace is created atomically and
   * returned.  This is the only call site for workspace bootstrapping.
   */
  async getUserWorkspaces(userId: string): Promise<WorkspaceResponseDto[]> {
    const memberships = await this.prisma.workspaceMember.findMany({
      where: { userId },
      include: {
        workspace: {
          select: { id: true, name: true, ownerId: true, createdAt: true },
        },
      },
      orderBy: { joinedAt: "asc" },
    });

    if (memberships.length > 0) {
      return memberships.map((m) => ({
        id: m.workspace.id,
        name: m.workspace.name,
        ownerId: m.workspace.ownerId,
        role: m.role,
        createdAt: m.workspace.createdAt,
      }));
    }

    // Auto-provision a default workspace for new users.
    // Re-query inside the transaction to guard against concurrent requests
    // both seeing zero memberships and creating duplicate workspaces.
    this.logger.log(`Auto-provisioning default workspace for user ${userId}`);

    const workspace = await this.prisma.$transaction(async (tx) => {
      const existing = await tx.workspaceMember.findFirst({
        where: { userId },
        include: {
          workspace: {
            select: { id: true, name: true, ownerId: true, createdAt: true },
          },
        },
      });
      if (existing) {
        return { ...existing.workspace, alreadyExisted: true as const };
      }

      const created = await tx.workspace.create({
        data: {
          name: "My Workspace",
          ownerId: userId,
          settings: {},
        },
      });
      await tx.workspaceMember.create({
        data: {
          workspaceId: created.id,
          userId,
          role: WorkspaceMemberRole.OWNER,
        },
      });
      return { ...created, alreadyExisted: false as const };
    });

    if (workspace.alreadyExisted) {
      return [
        {
          id: workspace.id,
          name: workspace.name,
          ownerId: workspace.ownerId,
          role: WorkspaceMemberRole.OWNER,
          createdAt: workspace.createdAt,
        },
      ];
    }

    return [
      {
        id: workspace.id,
        name: workspace.name,
        ownerId: workspace.ownerId,
        role: WorkspaceMemberRole.OWNER,
        createdAt: workspace.createdAt,
      },
    ];
  }

  /**
   * Add a member to a workspace.
   */
  async addMember(
    workspaceId: string,
    actorUserId: string,
    addMemberDto: AddMemberDto
  ): Promise<WorkspaceMember> {
    const actorMembership = await this.prisma.workspaceMember.findUnique({
      where: {
        workspaceId_userId: {
          workspaceId,
          userId: actorUserId,
        },
      },
      select: {
        role: true,
      },
    });

    if (!actorMembership) {
      throw new ForbiddenException("You are not a member of this workspace");
    }

    this.assertCanAssignRole(actorMembership.role, addMemberDto.role);

    const user = await this.prisma.user.findUnique({
      where: { id: addMemberDto.userId },
      select: { id: true },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${addMemberDto.userId} not found`);
    }

    const existingMembership = await this.prisma.workspaceMember.findUnique({
      where: {
        workspaceId_userId: {
          workspaceId,
          userId: addMemberDto.userId,
        },
      },
      select: {
        workspaceId: true,
        userId: true,
      },
    });

    if (existingMembership) {
      throw new ConflictException("User is already a member of this workspace");
    }

    try {
      return await this.prisma.workspaceMember.create({
        data: {
          workspaceId,
          userId: addMemberDto.userId,
          role: addMemberDto.role,
        },
      });
    } catch (error) {
      if (this.isUniqueConstraintError(error)) {
        throw new ConflictException("User is already a member of this workspace");
      }
      throw error;
    }
  }

  /**
   * Update the role of an existing workspace member.
   */
  async updateMemberRole(
    workspaceId: string,
    actorUserId: string,
    targetUserId: string,
    updateMemberRoleDto: UpdateMemberRoleDto
  ): Promise<WorkspaceMember> {
    return this.prisma.$transaction(async (tx) => {
      const actorMembership = await tx.workspaceMember.findUnique({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId: actorUserId,
          },
        },
        select: {
          role: true,
        },
      });

      if (!actorMembership) {
        throw new ForbiddenException("You are not a member of this workspace");
      }

      const targetMembership = await tx.workspaceMember.findUnique({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId: targetUserId,
          },
        },
        select: {
          role: true,
        },
      });

      if (!targetMembership) {
        throw new NotFoundException(`User ${targetUserId} is not a member of this workspace`);
      }

      this.assertCanManageTargetMember(actorMembership.role, targetMembership.role);
      this.assertCanAssignRole(actorMembership.role, updateMemberRoleDto.role);

      if (targetMembership.role === WorkspaceMemberRole.OWNER) {
        const isDemotion = updateMemberRoleDto.role !== WorkspaceMemberRole.OWNER;
        if (isDemotion) {
          const ownerCount = await tx.workspaceMember.count({
            where: {
              workspaceId,
              role: WorkspaceMemberRole.OWNER,
            },
          });
          if (ownerCount <= 1) {
            if (actorUserId === targetUserId) {
              throw new BadRequestException("Cannot self-demote if you are the sole owner");
            }
            throw new BadRequestException("Cannot remove the last owner from a workspace");
          }
        }
      }

      return tx.workspaceMember.update({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId: targetUserId,
          },
        },
        data: {
          role: updateMemberRoleDto.role,
        },
      });
    });
  }

  /**
   * Remove a member from a workspace.
   */
  async removeMember(
    workspaceId: string,
    actorUserId: string,
    targetUserId: string
  ): Promise<void> {
    await this.prisma.$transaction(async (tx) => {
      const actorMembership = await tx.workspaceMember.findUnique({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId: actorUserId,
          },
        },
        select: {
          role: true,
        },
      });

      if (!actorMembership) {
        throw new ForbiddenException("You are not a member of this workspace");
      }

      const targetMembership = await tx.workspaceMember.findUnique({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId: targetUserId,
          },
        },
        select: {
          role: true,
        },
      });

      if (!targetMembership) {
        throw new NotFoundException(`User ${targetUserId} is not a member of this workspace`);
      }

      this.assertCanManageTargetMember(actorMembership.role, targetMembership.role);

      if (targetMembership.role === WorkspaceMemberRole.OWNER) {
        const ownerCount = await tx.workspaceMember.count({
          where: {
            workspaceId,
            role: WorkspaceMemberRole.OWNER,
          },
        });
        if (ownerCount <= 1) {
          throw new BadRequestException("Cannot remove the last owner from a workspace");
        }
      }

      await tx.workspaceMember.delete({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId: targetUserId,
          },
        },
      });
    });
  }

  /**
   * Get members of a workspace.
   */
  async getMembers(workspaceId: string) {
    return this.prisma.workspaceMember.findMany({
      where: { workspaceId },
      include: {
        user: { select: { id: true, name: true, email: true, createdAt: true } },
      },
      orderBy: { joinedAt: "asc" },
    });
  }
  private assertCanAssignRole(
    actorRole: WorkspaceMemberRole,
    requestedRole: WorkspaceMemberRole
  ): void {
    if (WORKSPACE_ROLE_RANK[actorRole] < WORKSPACE_ROLE_RANK[requestedRole]) {
      throw new ForbiddenException("You cannot assign a role higher than your own");
    }
  }

  private assertCanManageTargetMember(
    actorRole: WorkspaceMemberRole,
    targetRole: WorkspaceMemberRole
  ): void {
    if (WORKSPACE_ROLE_RANK[actorRole] < WORKSPACE_ROLE_RANK[targetRole]) {
      throw new ForbiddenException("You cannot manage a member with a higher role");
    }
  }

  private isUniqueConstraintError(error: unknown): error is Prisma.PrismaClientKnownRequestError {
    return error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002";
  }

  async getStats(
    workspaceId: string
  ): Promise<{ memberCount: number; projectCount: number; domainCount: number }> {
    const [memberCount, projectCount, domainCount] = await Promise.all([
      this.prisma.workspaceMember.count({ where: { workspaceId } }),
      this.prisma.project.count({ where: { workspaceId } }),
      this.prisma.domain.count({ where: { workspaceId } }),
    ]);
    return { memberCount, projectCount, domainCount };
  }
}
