<?php

use events\BaseEvents;
use events\dto\UserEventsDto;
use events\EventService;

/**
 * @uri /user/([^\/]+)/subscription/query/([^\/]+) 3
 */
class UserSubscriptionQuery extends CfProtectedResource
{
    /**
     * @param $request
     * @param $username
     * @param $subscription_id
     *
     * @rbacName View report
     * @rbacGroup Scheduled reports
     * @rbacAlias userSubscriptionQuery.get
     * @rbacAllowedByDefault
     *
     * @return Response
     */
    public function get($request, $username, $subscription_id)
    {
        $user = $this->username;

        $response = new Response($request);

        $payload = cfapi_user_subscription_query_get($user, $username, $subscription_id);
        if ($payload) {
            $response->code = Response::OK;
            $response->body = $payload;
        } else {
            $response->code = Response::NOTFOUND;
        }

        return $response;
    }

    /**
     * @param $request
     * @param $username
     * @param $subscription_id
     *
     * @rbacName Create report
     * @rbacGroup Scheduled reports
     * @rbacAlias userSubscriptionQuery.put
     * @rbacAllowedByDefault
     *
     * @return Response
     * @throws ResponseException
     */
    public function put($request, $username, $subscription_id)
    {
        $user = $this->username;

        $data = Utils::getValidJsonData($request->data);
        $output_types = $data->outputTypes;
        if (!$output_types) {
            $output_types = array('csv');
        }

        if ($data->hostContextInclude) {
            if (!is_array($data->hostContextInclude)) {
                $response = new Response($request);
                $response->code = Response::BADREQUEST;
                $response->body = "'hostContextInclude' field must be an array";
                return $response;
            }
        } else {
            $data->hostContextInclude = array();
        }

        if ($data->hostContextExclude) {
            if (!is_array($data->hostContextExclude)) {
                $response = new Response($request);
                $response->code = Response::BADREQUEST;
                $response->body = "'hostContextExclude' field must be an array";
                return $response;
            }
        } else {
            $data->hostContextExclude = array();
        }

        $response = new Response($request);

        if (cfapi_user_subscription_query_put(
            $user,
            $username,
            $subscription_id,
            $data->to ?? '',
            $data->enabled,
            $data->query ?? '',
            $data->schedule ?? '',
            $data->title ?? '',
            $data->description ?? '',
            $output_types,
            $data->hostContextInclude ?? [],
            $data->hostContextExclude ?? []
        )) {
            $response->code = Response::CREATED;
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }

    /**
     * @param $request
     * @param $username
     * @param $subscription_id
     *
     * @rbacName Delete report
     * @rbacGroup Scheduled reports
     * @rbacAlias userSubscriptionQuery.delete
     * @rbacAllowedByDefault
     *
     * @return Response
     */
    public function delete($request, $username, $subscription_id)
    {
        $user = $this->username;

        $response = new Response($request);

        if (cfapi_user_subscription_query_delete($user, $username, $subscription_id)) {
            $response->code = Response::ACCEPTED;
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }
}

/**
 * @uri /user/(.+)/subscription/query 2
 */
class UserSubscriptionQueryList extends CfProtectedResource
{
    /**
     * @param $request
     * @param $username
     *
     * @rbacName View report list
     * @rbacGroup Scheduled reports
     * @rbacAlias userSubscriptionQueryList.get
     * @rbacAllowedByDefault
     *
     * @return Response
     */
    public function get($request, $username)
    {
        $user = $this->username;

        $response = new Response($request);
        $payload = cfapi_user_subscription_query_list($user, $username);
        if ($payload) {
            $response->code = Response::OK;
            $response->body = $payload;
        } else {
            $response->code = Response::NOTFOUND;
        }

        return $response;
    }
}


const CF_PASSWORD_RESET_HEADER = 'Cf-Password-Reset';
/**
 * @uri /user/:username 1
 */
class User extends CfProtectedResource
{
    /**
     * @param $request
     * @param $username
     *
     * @rbacName View users info
     * @rbacGroup User
     * @rbacAlias user.get
     * @rbacAllowedByDefault
     * @rbacSkipControllerCheck
     *
     * @return Response
     */
    public function get($request, $username)
    {
        $user = $this->username;

        $permissions = (new CfRBAC())->getPermissionsByRoles((new CfUsers($user))->getUserRoles());
        $getUserPermission = RbacAccessService::isActionAllowed('user.get', $permissions);

        $response = new Response($request);
        $payload = cfapi_user_get($user, $username, $getUserPermission);

        if ($payload) {
            $payload = json_decode($payload, JSON_OBJECT_AS_ARRAY);
            $isTwoFaEnforced = (CfSettings::getInstance())->getSetting(SettingsHelper::ENFORCE_2FA);
            if (isset($payload['data'][0])) {
                $payload['data'][0]['locked'] = !$payload['data'][0]['two_factor_enabled'] && (new CfUsers($username))->isUserLockedByTwoFaEnforcement($isTwoFaEnforced);
            }
            $response->code = Response::OK;
            $response->body = json_encode($payload);
        } else {
            $response->code = Response::NOTFOUND;
        }

        return $response;
    }

    /**
     * @param $request
     * @param $username
     *
     * @rbacName Create user
     * @rbacGroup User
     * @rbacAlias user.put
     *
     * @return Response
     * @throws ResponseException
     */
    public function put($request, $username)
    {
        $user = $this->username;

        $data = Utils::getValidJsonData($request->data);

        $response = new Response($request);

        $this->validate($data);

        if (cfapi_user_put(
            $user,
            $username,
            $data->password ?? '',
            $data->name ?? '',
            $data->email ?? '',
            $data->time_zone ?? '',
            isset($data->roles) && is_array($data->roles) ? array_unique($data->roles) : null
        )
        ) {
            $response->code = Response::CREATED;

            AuditLogService::register([
                AuditLogFields::ACTOR => $this->username,
                AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
                AuditLogFields::OBJECT_ID => $username,
                AuditLogFields::OBJECT_NAME => $username,
                AuditLogFields::ACTION => AuditLogActions::CREATE,
                AuditLogFields::DETAILS => ["Created user `$username`."]
            ]);
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }

    /**
     * @param $request
     * @param $username
     *
     * @rbacName Update user
     * @rbacGroup User
     * @rbacAlias user.post
     *
     * @return Response
     * @throws ResponseException
     */
    public function post($request, $username)
    {
        $user = $this->username;
        $headers = getallheaders();

        $data = Utils::getValidJsonData($request->data);

        $response = new Response($request);

        $this->validate($data, $username);

        $isPasswordChanged = property_exists($data, 'password');


        if ($isPasswordChanged) {
            TwoFaHelper::verifyRequest((new CfUsers($this->username)), $headers);
        }

        $permissions = (new CfRBAC())->getPermissionsByRoles((new CfUsers($user))->getUserRoles());
        $updatePermission = RbacAccessService::isActionAllowed('user.post', $permissions);
        $getUserPermission = RbacAccessService::isActionAllowed('user.get', $permissions);

        $payload = cfapi_user_get($user, $username, $getUserPermission);
        $currentData = (array) (json_decode($payload, true)['data'][0] ?? []);
        $newData = (array) $data;
        $changedKeys = array_keys(
            array_diff(
                array_map('serialize', $newData),
                array_map('serialize', $currentData)
            )
        );


        if (cfapi_user_post(
            $user,
            $username,
            $data->password ?? '',
            $data->name ?? '',
            $data->email ?? '',
            $data->time_zone ?? '',
            isset($data->roles) && is_array($data->roles) ? array_unique($data->roles) : null,
            $updatePermission
        )) {
            $response->code = Response::ACCEPTED;

            if ($isPasswordChanged) {
                EventService::dispatchEvent(
                    BaseEvents::PASSWORD_CHANGED,
                    new UserEventsDto(
                        username: $username,
                        oauthToken: $this->accessToken,
                        isPasswordReset: (isset($headers[CF_PASSWORD_RESET_HEADER]) && $headers[CF_PASSWORD_RESET_HEADER] === 'true'),
                        cfUser: new CfUsers($username)
                    )
                );
            }

            EventService::dispatchEvent(
                BaseEvents::USER_UPDATED,
                new UserEventsDto(
                    username: $username,
                    cfUser: new CfUsers($username),
                    updatedData: (array) $data
                )
            );

            AuditLogService::register([
                AuditLogFields::ACTOR => $this->username,
                AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
                AuditLogFields::OBJECT_ID => $username,
                AuditLogFields::OBJECT_NAME => $username,
                AuditLogFields::ACTION => AuditLogActions::UPDATE,
                AuditLogFields::DETAILS => ["Updated user `$username`.", ['changed properties' => $changedKeys]]
            ]);
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }

    /**
     * @param $request
     * @param $username
     *
     * @rbacName Delete user
     * @rbacGroup User
     * @rbacAlias user.delete
     *
     * @return Response
     */
    public function delete($request, $username)
    {
        $user = $this->username;

        $response = new Response($request);

        if (cfapi_user_delete($user, $username)) {
            $response->code = Response::ACCEPTED;
            AuditLogService::register([
                AuditLogFields::ACTOR => $this->username,
                AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
                AuditLogFields::OBJECT_ID => $username,
                AuditLogFields::OBJECT_NAME => $username,
                AuditLogFields::ACTION => AuditLogActions::DELETE,
                AuditLogFields::DETAILS => ["Deleted user `$username`."]
            ]);
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }


    /**
     * @param $data
     * @throws InvalidArgumentException
     * @return void
     *
     * Throws InvalidArgumentException in case of not valid input data
     */
    private function validate($data, $username = null): void
    {
        if (isset($data->time_zone)) {
            Validator::timezone($data->time_zone);
        }

        if (property_exists($data, 'password')) {
            Validator::password($data->password, $username);
        }

        if (isset($data->roles) && is_array($data->roles)) {
            Validator::rolesExists($data->roles);
        }
    }
}

/**
 * @uri /user/:username/unlock 1
 */
class UnlockUser extends CfProtectedResource
{
    /**
     * @param $request
     * @param $username
     *
     * @rbacName Unlock user
     * @rbacGroup User
     * @rbacAlias user.unlock
     *
     * @return Response
     * @throws ResponseException
     */
    public function post($request, $username)
    {
        $response = new Response($request);
        $cfUser = new CfUsers($username);

        if (!$cfUser->getUser()) {
            $response->code = Response::NOTFOUND;
        }

        $cfUser->resetFirstLoginAfter2faEnforced();
        $response->code = Response::ACCEPTED;

        AuditLogService::register([
            AuditLogFields::ACTOR => $this->username,
            AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
            AuditLogFields::OBJECT_ID => $username,
            AuditLogFields::OBJECT_NAME => $username,
            AuditLogFields::ACTION => AuditLogActions::UPDATE,
            AuditLogFields::DETAILS => ["Unlocked user `$username`."]
        ]);

        return $response;
    }
}

/**
 * @uri /user 0
 */
class UserList extends CfProtectedResource
{
    /**
     * @param $request
     *
     * @rbacName User list
     * @rbacGroup User
     * @rbacAlias userList.get
     * @rbacAllowedByDefault
     *
     * @return Response
     * @throws ResponseException
     */
    public function get($request)
    {

        $this->accessRequired();
        $user = $this->username;

        $username_rx = Utils::queryParam('id');
        $determineInactiveUsers = Utils::queryParam('determineInactiveUsers', defaultValue: false);
        $external = Utils::checkBoolean(Utils::queryParam('external'), 'external');
        $userModel = new UsersList();

        $response = new Response($request);
        $isTwoFaEnforced = (CfSettings::getInstance())->getSetting(SettingsHelper::ENFORCE_2FA);

        $userListRequest = new UsersListRequestStructure(
            $user,
            $username_rx,
            userType: $external ? 'external' : 'internal',
            isTwoFaEnforced: $isTwoFaEnforced,
            determineInactiveUsers: $determineInactiveUsers
        );
        $response->body = $userModel->getUsersList($userListRequest);
        $response->code = Response::OK;

        return $response;
    }
}

/**
 * @uri /me/change-password
 *
 * This functionality changes only current user password.
 * Other fields cannot be changed.
 *
 * This is protected resource and you should be already authenticated.
 */
class UpdateOwnPassword extends CfProtectedResource
{
    /**
     * @param $request
     *
     * @rbacName Update own profile
     * @rbacDescription Allows users to change their own password or time zone
     * @rbacGroup User
     * @rbacAlias user.update_own_profile
     * @rbacAllowedByDefault
     *
     * @return Response
     * @throws ResponseException
     */
    public function post($request)
    {
        $username = $this->username;
        $headers = getallheaders();

        $data = Utils::getValidJsonData($request->data);
        $response = new Response($request);

        /**
         * Check if old password is correct or not
         */
        if (property_exists($data, 'old_password')) {
            if (!CFAuthService::checkPasswordValid($username, $data->old_password)) {
                throw new InvalidArgumentException('Username-password combination is wrong');
            }
        } else {
            throw new InvalidArgumentException('Old password is required');
        }

        if (property_exists($data, 'password')) {
            Validator::password($data->password, $username);
        } else {
            throw new InvalidArgumentException('Password is required');
        }

        TwoFaHelper::verifyRequest((new CfUsers($username)), $headers);

        if (cfapi_user_post(
            $username,
            $username,
            $data->password,
            $name = '',
            $email = '',
            $time_zone = '',
            $roles = null,
            true
        )) {
            $response->code = Response::ACCEPTED;

            EventService::dispatchEvent(
                BaseEvents::PASSWORD_CHANGED,
                new UserEventsDto(
                    username: $username,
                    oauthToken: $this->accessToken,
                    isPasswordReset: (isset($headers[CF_PASSWORD_RESET_HEADER]) && $headers[CF_PASSWORD_RESET_HEADER] === 'true'),
                    cfUser: new CfUsers($username)
                )
            );
            EventService::dispatchEvent(
                BaseEvents::USER_UPDATED,
                new UserEventsDto(
                    username: $username,
                    cfUser: new CfUsers($username),
                    updatedData: ['password' => true]
                )
            );

            AuditLogService::register([
                AuditLogFields::ACTOR => $username,
                AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
                AuditLogFields::OBJECT_ID => $username,
                AuditLogFields::OBJECT_NAME => $username,
                AuditLogFields::ACTION => AuditLogActions::UPDATE,
                AuditLogFields::DETAILS => ["User `$username` updated own password."]
            ]);
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }
}


/**
 * @uri /me
 *
 * This functions changes user own profiles
 * Supported fields are: time_zone
 *
 * This is protected resource and you should be already authenticated.
 */
class UpdateOwnProfile extends CfProtectedResource
{
    /**
     * @param $request
     *
     * @rbacName Update own profile
     * @rbacDescription Allows users to change their own password or time zone
     * @rbacGroup User
     * @rbacAlias user.update_own_profile
     * @rbacAllowedByDefault
     *
     * @return Response
     * @throws ResponseException
     */
    public function post($request)
    {
        $username = $this->username;
        $data = Utils::getValidJsonData($request->data);
        $response = new Response($request);

        if (property_exists($data, 'time_zone')) {
            Validator::timezone($data->time_zone);
        } else {
            $response->code = Response::BADREQUEST;
            return $response;
        }

        if (cfapi_user_post(
            $username,
            $username,
            $password = '',
            $name = '',
            $email = '',
            $data->time_zone ?? '',
            $roles = null,
            true
        )) {
            $response->code = Response::ACCEPTED;

            EventService::dispatchEvent(
                BaseEvents::USER_UPDATED,
                new UserEventsDto(
                    username: $username,
                    cfUser: new CfUsers($username),
                    updatedData: ['time_zone' => true]
                )
            );

            AuditLogService::register([
                AuditLogFields::ACTOR => $username,
                AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
                AuditLogFields::OBJECT_ID => $username,
                AuditLogFields::OBJECT_NAME => $username,
                AuditLogFields::ACTION => AuditLogActions::UPDATE,
                AuditLogFields::DETAILS => ["User `$username` updated own profile."]
            ]);
        } else {
            $response->code = Response::INTERNALSERVERERROR;
        }

        return $response;
    }
}
