import { CustomError } from '../helpers/common.ts'
import { SYSTEM_PERMISSION } from '../models/groups.ts'
import type { FastifyInstance, FastifyRequest } from 'fastify'
import type { GroupPatch, GroupRule, GroupWithUserCount } from '../models/groups.ts'

/**
 * Refuse a `manage:groups` holder any change to who is in a group that carries `manage:system`.
 *
 * Membership of such a group IS the permission: adding somebody hands them the root of the instance,
 * and removing somebody takes it away from a real administrator. Deleting the group does both at
 * once, so it asks the same question.
 *
 * @param action What the caller was trying to do, as the message reads it back to them
 * @returns The refusal to throw, or null when the caller may proceed
 */
function systemGroupGuard(
  req: FastifyRequest,
  group: GroupWithUserCount,
  action = 'change who belongs to the group'
): CustomError | null {
  if (!group.permissions.includes(SYSTEM_PERMISSION)) {
    return null
  }
  if (WIKI.models.groups.holdsSystemPermission(req)) {
    return null
  }
  return new CustomError(
    'groupMembershipSystemProtected',
    `This group has the ${SYSTEM_PERMISSION} permission. Only a user who holds it can ${action}.`,
    403
  )
}

interface GroupUpdateBody {
  name?: string
  redirectOnLogin?: string
  redirectOnFirstLogin?: string
  redirectOnLogout?: string
  permissions?: string[]
  rules?: GroupRule[]
}

/**
 * Groups API Routes
 */
async function routes(app: FastifyInstance) {
  /**
   * LIST ALL GROUPS
   */
  app.get(
    '/',
    {
      config: {
        // -> `manage:navigation` is here because a menu item can be limited to groups, so the
        //    navigation editor has to be able to name them. It is safe to grant on this route and this
        //    route only: the listing is `GroupCore`, which carries no permissions, no rules and no
        //    members — reading one group in full, or its members, keeps needing `manage:groups`.
        permissions: ['read:groups', 'manage:groups', 'manage:navigation']
      },
      schema: {
        summary: 'List all groups',
        description:
          'Every group by id and name, with its member count. Nothing about what a group may do or who is in it — that is `GET /groups/{groupId}`.',
        tags: ['Groups'],
        response: {
          200: {
            description: 'List of all groups',
            type: 'array',
            items: { $ref: 'GroupCore#' }
          }
        }
      }
    },
    async () => {
      return WIKI.models.groups.getAllGroups()
    }
  )

  /**
   * CREATE GROUP
   */
  app.post<{ Body: { name: string } }>(
    '/',
    {
      config: {
        permissions: ['write:groups', 'manage:groups']
      },
      schema: {
        summary: 'Create a new group',
        description:
          'Creates a non-system group, seeded with the same starting permissions and default rule as the built-in `Users` group.',
        tags: ['Groups'],
        body: {
          type: 'object',
          required: ['name'],
          properties: {
            name: {
              type: 'string',
              minLength: 1,
              maxLength: 255
            }
          },
          examples: [{ name: 'Editors' }]
        },
        response: {
          200: {
            description: 'Group created successfully',
            type: 'object',
            properties: {
              ok: {
                type: 'boolean'
              },
              message: {
                type: 'string'
              },
              id: {
                type: 'string',
                format: 'uuid'
              }
            }
          }
        }
      }
    },
    async (req, reply) => {
      if (!/^[^<>"]+$/.test(req.body.name)) {
        throw new CustomError('groupCreateInvalidName', 'Invalid Group Name')
      }

      try {
        const id = await WIKI.models.groups.createGroup(req.body.name)
        return {
          ok: true,
          message: 'Group created successfully.',
          id
        }
      } catch (err: any) {
        WIKI.logger.warn(err)
        return reply.internalServerError()
      }
    }
  )

  /**
   * GET SINGLE GROUP
   */
  app.get<{ Params: { groupId: string } }>(
    '/:groupId',
    {
      config: {
        permissions: ['read:groups', 'manage:groups']
      },
      schema: {
        summary: 'Get a single group',
        description: 'Returns the group with its full permissions and page rules.',
        tags: ['Groups'],
        params: {
          type: 'object',
          properties: {
            groupId: {
              type: 'string',
              format: 'uuid'
            }
          },
          required: ['groupId']
        },
        response: {
          200: {
            description: 'Group info',
            type: 'object',
            $ref: 'Group#'
          }
        }
      }
    },
    async (req, reply) => {
      const group = await WIKI.models.groups.getGroupById(req.params.groupId)
      if (!group) {
        return reply.notFound('Group does not exist.')
      }
      return group
    }
  )

  /**
   * UPDATE GROUP
   */
  app.put<{ Params: { groupId: string }; Body: GroupUpdateBody }>(
    '/:groupId',
    {
      config: {
        permissions: ['write:groups', 'manage:groups']
      },
      schema: {
        summary: 'Update a group',
        description:
          'Updates any subset of the group fields. Omitted fields are left unchanged. The permissions of the root administrators group cannot be modified.',
        tags: ['Groups'],
        params: {
          type: 'object',
          properties: {
            groupId: {
              type: 'string',
              format: 'uuid'
            }
          },
          required: ['groupId']
        },
        body: {
          type: 'object',
          properties: {
            name: {
              type: 'string',
              minLength: 1,
              maxLength: 255
            },
            redirectOnLogin: {
              type: 'string',
              maxLength: 255
            },
            redirectOnFirstLogin: {
              type: 'string',
              maxLength: 255
            },
            redirectOnLogout: {
              type: 'string',
              maxLength: 255
            },
            permissions: {
              type: 'array',
              items: {
                type: 'string'
              }
            },
            rules: {
              type: 'array',
              items: { $ref: 'GroupRule#' }
            }
          },
          examples: [
            {
              name: 'Editors',
              permissions: ['read:pages', 'write:pages']
            }
          ]
        },
        response: {
          200: {
            description: 'Group updated successfully',
            type: 'object',
            properties: {
              ok: {
                type: 'boolean'
              },
              message: {
                type: 'string'
              }
            }
          }
        }
      }
    },
    async (req, reply) => {
      const group = await WIKI.models.groups.getGroupById(req.params.groupId)
      if (!group) {
        return reply.notFound('Group does not exist.')
      }

      // -> Collect only the fields actually provided
      const patch: GroupPatch = {}
      if (req.body.name !== undefined) {
        patch.name = req.body.name
      }
      if (req.body.redirectOnLogin !== undefined) {
        patch.redirectOnLogin = req.body.redirectOnLogin
      }
      if (req.body.redirectOnFirstLogin !== undefined) {
        patch.redirectOnFirstLogin = req.body.redirectOnFirstLogin
      }
      if (req.body.redirectOnLogout !== undefined) {
        patch.redirectOnLogout = req.body.redirectOnLogout
      }
      if (req.body.permissions !== undefined) {
        patch.permissions = req.body.permissions
      }
      if (req.body.rules !== undefined) {
        patch.rules = req.body.rules
      }

      if (Object.keys(patch).length < 1) {
        throw new CustomError('groupUpdateEmpty', 'No group fields provided to update.')
      }

      // -> The root administrators group must keep its permissions, or the instance becomes
      //    unmanageable with no way to grant `manage:system` back. Resending the current set is
      //    allowed, so that a client editing other fields can still submit the whole group.
      if (patch.permissions && group.id === WIKI.config.auth.rootAdminGroupId) {
        const isUnchanged =
          patch.permissions.length === group.permissions.length &&
          patch.permissions.every((p) => group.permissions.includes(p))
        if (!isUnchanged) {
          throw new CustomError(
            'groupUpdateRootAdminPermissions',
            'Cannot modify the permissions of the root administrators group.'
          )
        }
      }

      /*
        A `manage:groups` holder may edit a group that carries `manage:system` -- name, rules,
        redirects, every other permission -- but may not turn that one permission on or off. Granting
        it is handing over the instance; revoking it is locking the real administrators out.
      */
      if (patch.permissions && !WIKI.models.groups.holdsSystemPermission(req)) {
        const held = group.permissions.includes(SYSTEM_PERMISSION)
        if (held !== patch.permissions.includes(SYSTEM_PERMISSION)) {
          throw new CustomError(
            'groupUpdateSystemPermission',
            `Only a user who holds the ${SYSTEM_PERMISSION} permission can grant or revoke it. Every other change to this group is allowed.`,
            403
          )
        }
      }

      // -> Rule IDs must be unique within the group, as they address the rule client-side
      if (patch.rules) {
        const ruleIds = patch.rules.map((r) => r.id)
        if (new Set(ruleIds).size !== ruleIds.length) {
          throw new CustomError('groupUpdateDuplicateRuleId', 'Group rule IDs must be unique.')
        }
      }

      try {
        await WIKI.models.groups.updateGroup(group.id, patch)
        return {
          ok: true,
          message: 'Group updated successfully.'
        }
      } catch (err: any) {
        WIKI.logger.warn(err)
        return reply.internalServerError()
      }
    }
  )

  /**
   * DELETE GROUP
   */
  app.delete<{ Params: { groupId: string } }>(
    '/:groupId',
    {
      config: {
        permissions: ['manage:groups']
      },
      schema: {
        summary: 'Delete a group',
        description:
          'Deletes the group and removes all of its user assignments. System groups cannot be deleted.',
        tags: ['Groups'],
        params: {
          type: 'object',
          properties: {
            groupId: {
              type: 'string',
              format: 'uuid'
            }
          },
          required: ['groupId']
        },
        response: {
          204: {
            description: 'Group deleted successfully'
          }
        }
      }
    },
    async (req, reply) => {
      const group = await WIKI.models.groups.getGroupById(req.params.groupId)
      if (!group) {
        return reply.notFound('Group does not exist.')
      }
      if (group.isSystem) {
        return reply.conflict('Cannot delete a system group.')
      }

      // -> Deleting the group removes every member from it, so it is the membership guard's question
      const systemGroupRefusal = systemGroupGuard(req, group, 'delete the group')
      if (systemGroupRefusal) {
        throw systemGroupRefusal
      }

      try {
        await WIKI.models.groups.deleteGroup(group.id)
        return reply.code(204).send()
      } catch (err: any) {
        WIKI.logger.warn(err)
        return reply.internalServerError()
      }
    }
  )

  /**
   * LIST GROUP USERS
   */
  app.get<{
    Params: { groupId: string }
    Querystring: { filter?: string; page?: number; limit?: number }
  }>(
    '/:groupId/users',
    {
      config: {
        permissions: ['read:groups', 'manage:groups']
      },
      schema: {
        summary: 'List the users assigned to a group',
        description: 'Returns a page of group members, ordered by name.',
        tags: ['Groups'],
        params: {
          type: 'object',
          properties: {
            groupId: {
              type: 'string',
              format: 'uuid'
            }
          },
          required: ['groupId']
        },
        querystring: {
          type: 'object',
          properties: {
            filter: {
              type: 'string',
              description: 'Case-insensitive substring matched against the name and email.',
              maxLength: 255
            },
            page: { type: 'integer', minimum: 1, default: 1 },
            limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 }
          }
        },
        response: {
          200: {
            description: 'List of group members',
            type: 'object',
            properties: {
              page: { type: 'integer' },
              limit: { type: 'integer' },
              total: { type: 'integer' },
              users: {
                type: 'array',
                items: { $ref: 'UserCore#' }
              }
            }
          }
        }
      }
    },
    async (req, reply) => {
      const group = await WIKI.models.groups.getGroupById(req.params.groupId)
      if (!group) {
        return reply.notFound('Group does not exist.')
      }

      const page = req.query.page ?? 1
      const limit = req.query.limit ?? 20
      const { total, users } = await WIKI.models.groups.getGroupUsers(group.id, {
        filter: req.query.filter,
        page,
        limit
      })

      return { page, limit, total, users }
    }
  )

  /**
   * ASSIGN USER TO GROUP
   */
  app.post<{ Params: { groupId: string; userId: string } }>(
    '/:groupId/users/:userId',
    {
      config: {
        permissions: ['write:groups', 'manage:groups']
      },
      schema: {
        summary: 'Assign a user to a group',
        description:
          'System users (the guest account) cannot be assigned: their group membership is fixed at install time.',
        tags: ['Groups'],
        params: {
          type: 'object',
          properties: {
            groupId: {
              type: 'string',
              format: 'uuid'
            },
            userId: {
              type: 'string',
              format: 'uuid'
            }
          },
          required: ['groupId', 'userId']
        },
        response: {
          200: {
            description: 'User assigned successfully',
            type: 'object',
            properties: {
              ok: {
                type: 'boolean'
              },
              message: {
                type: 'string'
              }
            }
          }
        }
      }
    },
    async (req, reply) => {
      const group = await WIKI.models.groups.getGroupById(req.params.groupId)
      if (!group) {
        return reply.notFound('Group does not exist.')
      }
      const user = await WIKI.models.users.getById(req.params.userId)
      if (!user) {
        return reply.notFound('User does not exist.')
      }

      const systemGroupRefusal = systemGroupGuard(req, group)
      if (systemGroupRefusal) {
        throw systemGroupRefusal
      }

      /*
        The guests group and the guest account belong to each other and to nothing else — the group is
        what anonymous visitors hold, and the account is who they are. `guestMembershipViolation` is
        the one definition of that, shared with `setUserGroups`, which is what the user editor and
        provider enrolment go through.
      */
      const violation = WIKI.models.groups.guestMembershipViolation(group.id, user)
      if (violation) {
        return reply.conflict(violation)
      }

      const assigned = await WIKI.models.groups.assignUserToGroup(group.id, req.params.userId)
      if (!assigned) {
        return reply.conflict('User is already assigned to this group.')
      }

      return {
        ok: true,
        message: 'User assigned to group successfully.'
      }
    }
  )

  /**
   * UNASSIGN USER FROM GROUP
   */
  app.delete<{ Params: { groupId: string; userId: string } }>(
    '/:groupId/users/:userId',
    {
      config: {
        permissions: ['write:groups', 'manage:groups']
      },
      schema: {
        summary: 'Unassign a user from a group',
        description:
          'Removes the user from the group. The last remaining user cannot be removed from the root administrators group, and system users (the guest account) cannot be unassigned at all.',
        tags: ['Groups'],
        params: {
          type: 'object',
          properties: {
            groupId: {
              type: 'string',
              format: 'uuid'
            },
            userId: {
              type: 'string',
              format: 'uuid'
            }
          },
          required: ['groupId', 'userId']
        },
        response: {
          204: {
            description: 'User unassigned successfully'
          }
        }
      }
    },
    async (req, reply) => {
      const group = await WIKI.models.groups.getGroupById(req.params.groupId)
      if (!group) {
        return reply.notFound('Group does not exist.')
      }
      if (!(await WIKI.models.groups.isUserInGroup(group.id, req.params.userId))) {
        return reply.notFound('User is not assigned to this group.')
      }

      const systemGroupRefusal = systemGroupGuard(req, group)
      if (systemGroupRefusal) {
        throw systemGroupRefusal
      }

      // -> Removing the guest account from the guests group would strip anonymous visitors of the
      //    permissions that group carries, with no way to put it back. `unassignUserFromGroup`
      //    refuses that pair as well; this answers it as a conflict rather than as a failure.
      const user = await WIKI.models.users.getById(req.params.userId)
      if (user?.isSystem) {
        return reply.conflict('Cannot unassign a system user from a group.')
      }

      // -> Emptying the root administrators group would lock everyone out of system management
      if (group.id === WIKI.config.auth.rootAdminGroupId) {
        if ((await WIKI.models.groups.countUsersInGroup(group.id)) <= 1) {
          return reply.conflict('Cannot remove the last user from the root administrators group.')
        }
      }

      await WIKI.models.groups.unassignUserFromGroup(group.id, req.params.userId)
      return reply.code(204).send()
    }
  )
}

export default routes
