<?php

namespace Velis\Acl;

use ArrayObject;
use LogicException;
use Psr\SimpleCache\InvalidArgumentException;
use Velis\Model\DataObject;
use Velis\Model\Cacheable;
use Velis\Arrays;
use Velis\Filter;
use User\User;
use Velis\Lang;
use Velis\App;
use Velis\User as VelisUser;

/**
 * Acl role (user group)
 * @author Olek Procki <olo@velis.pl>
 */
class Role extends DataObject implements Cacheable
{
    /**
     * Returns related table name
     * @return string
     */
    protected function _getTableName()
    {
        return 'acl.role_tab';
    }


    /**
     * Return full roles list
     *
     * @param int $page
     * @param array|ArrayObject $params
     * @param string $order
     * @param int $limit
     * @param string|array $fields
     *
     * @return Role[]
     * @throws DataObject\NoColumnsException
     */
    public static function getList($page = null, $params = null, $order = null, $limit = null, $fields = null)
    {
        $roles = array();
        foreach (self::$_db->getAll("SELECT * FROM acl.role_tab ORDER BY name_" . (Lang::$lang ?: 'pl')) as $row) {
            $roles[$row['role_id']] = new self($row);
        }

        return $roles;
    }


    /**
     * Returns cached instance(s) (Must implement Cacheable)
     *
     * @param mixed $objectId
     * @return DataObject|DataObject[]
     *
     * @throws LogicException|InvalidArgumentException
     */
    public static function get($objectId)
    {
        $result = array();

        if (is_array($objectId)) {
            if (Filter::validateInts($objectId)) {
                return parent::get($objectId);
            } else {
                foreach (self::listCached() as $key => $role) {
                    if (in_array($role->getAcro(), $objectId)) {
                        $result[$key] = $role;
                    }
                }
                return $result;
            }
        } else {
            if (Filter::validateInt($objectId)) {
                return parent::get($objectId);
            } else {
                return self::byAcro($objectId);
            }
        }
    }


    /**
     * Returns role name
     * @return string
     */
    public function getName()
    {
        return $this->getTranslatedName() ?: $this->getAcro();
    }


    /**
     * Returns object as string
     * @return string
     */
    public function __toString()
    {
        return $this->getName() . '';
    }


    /**
     * Returns role acronym
     * @return string
     */
    public function getAcro()
    {
        return self::hasField('acro') ? $this['acro'] : $this['role_id'];
    }


    /**
     * Return role privs
     * @return Priv[]
     */
    public function getPrivs()
    {
        $query = "SELECT p.* FROM acl.role_priv_tab AS rp, acl.priv_tab AS p
                  WHERE rp.role_id = p.role_id ORDER BY p.acro";

        $privs = array();

        foreach (self::$_db->getAll($query) as $row) {
            if (Priv::hasField('priv_acro') && Priv::hasField('app_module_id')) {
                $privs[$row['priv_acro']][$row['app_module_id']] = new Priv($row);
            } else {
                $privs[$row['priv_id']] = new Priv($row);
            }
        }
        return $privs;
    }


    /**
     * Returns role users
     *
     * @param bool $activeOnly
     * @return User[]
     */
    public function getUsers($activeOnly = false)
    {
        $query   = "SELECT u.user_id FROM acl.user_role_tab AS ur, acl.user_tab AS u
                    WHERE ur.user_id = u.user_id AND ur.role_id=:role_id";

        if ($activeOnly) {
            $query .= " AND u.active=1";
        }

        $userIds = Arrays::getColumn(self::$_db->getAll($query, array('role_id' => $this->id())), 'user_id');

        return User::listById($userIds);
    }


    /**
     * Returns role users with passed role acronym
     *
     * @param bool $activeOnly
     * @return User[]
     */
    public static function getUsersWithRole($acro, $activeOnly = false)
    {
        if ($role = self::byAcro($acro)) {
            return $role->getUsers($activeOnly);
        } else {
            return array();
        }
    }


    /**
     * Returns roles assigned to user
     *
     * @param User|int $user
     * @return Role
     */

    public static function byUser($user)
    {
        $params = array (
            'user_id' => $user instanceof VelisUser ? $user->id() : $user
        );

        $query = "SELECT r.* FROM acl.role_tab r, acl.user_role_tab ur WHERE
                  r.role_id=ur.role_id AND ur.user_id = :user_id ORDER BY" . (self::hasField('acro') ? " r.acro" : " r.role_id");

        $roles = array();
        $key = self::hasField('acro') ? "acro" : "role_id";
        $restore = self::$_db->checkDuplicatedQueries(false);
        foreach (self::$_db->getAll($query, $params) as $row) {
            $roles[$row[$key]] = new self($row);
        }
        self::$_db->checkDuplicatedQueries($restore);

        return $roles;
    }


    /**
     * Returns role instance by acronym
     *
     * @param string $acro
     * @return Role
     */
    public static function byAcro($acro)
    {
        $key = self::hasField('acro') ? 'acro' : 'role_id';
        foreach (self::listCached() as $role) {
            if ($role[$key] == $acro) {
                return $role;
            }
        }
    }


    /**
     * Deletes role & relations
     * @return bool
     */
    public function remove()
    {
        self::$_db->startTrans();

        $params = array('role_id' => $this->id());

        self::$_db->execDML("DELETE FROM acl.user_role_tab WHERE role_id=:role_id", $params);
        self::$_db->execDML("DELETE FROM acl.role_priv_tab WHERE role_id=:role_id", $params);

        parent::_remove();

        self::$_db->commit();

        return true;
    }


    /**
     * Assigns user role
     *
     * @param User|int $user
     * @param bool $handleConflict
     * @return Role
     */
    public function assign($user, $handleConflict = false)
    {
        $sql = "INSERT INTO acl.user_role_tab (role_id, user_id) VALUES (:role_id, :user_id)";
        $params = [
            'role_id' => $this->id(),
            'user_id' => $user instanceof User ? $user->id() : $user
        ];

        if ($handleConflict) {
            $sql .= " ON CONFLICT DO NOTHING";
        }
        self::$_db->execDML($sql, $params);
        return $this;
    }


    /**
     * Revoke role from user
     * @param User $user
     */
    public function revoke($user)
    {
        self::$_db->execDML(
            'DELETE FROM acl.user_role_tab WHERE user_id=:user_id AND role_id=:role_id',
            array (
                'role_id' => $this->id(),
                'user_id' => $user instanceof User ? $user->id() : $user
            )
        );
    }


    /**
     * Revoke all roles
     * @param User $user
     */
    public static function revokeAll($user)
    {
        self::$_db->execDML(
            'DELETE FROM acl.user_role_tab WHERE user_id=:user_id',
            array ('user_id' => $user->id())
        );
    }


    /**
     * Returns true if role is assigned to $user
     *
     * @param User $user
     * @return bool
     */
    public function assigned($user)
    {
        $params = array (
            'role_id' => $this->id(),
            'user_id' => $user instanceof User ? $user->id() : $user
        );
        $query = "SELECT COUNT(*) FROM acl.user_role_tab WHERE role_id=:role_id AND user_id=:user_id";
        return self::$_db->getOne($query, $params) > 0;
    }


    /**
     * Return active roles list
     * @return Role[]
     */
    public static function listActive($internal = null)
    {
        $roles = self::listCached();

        if (self::hasField('is_active')) {
            foreach ($roles as $index => $role) {
                if ($role['is_active'] == 0) {
                    unset($roles[$index]);
                }
            }
        }

        if (!App::isSuper(true) && array_key_exists('SupportIT', $roles)) {
            unset($roles['SupportIT']);
        }
        return $roles;
    }


    /**
     * Returns internal roles list
     * @params boolean
     * @return Role[]
     */
    public static function listInternal($includeInactive = false)
    {
        if ($includeInactive) {
            $roles = self::listCached();
        } else {
            $roles = self::listActive();
        }

        foreach ($roles as $acro => $role) {
            if ($role->is_external) {
                unset($roles[$acro]);
            }
        }
        return $roles;
    }

    /**
     * Returns external roles list
     * @params boolean
     * @return Role[]
     */
    public static function listExternal($includeInactive = false)
    {
        if ($includeInactive) {
            $roles = self::listCached();
        } else {
            $roles = self::listActive();
        }

        foreach ($roles as $acro => $role) {
            if (!$role->is_external) {
                unset($roles[$acro]);
            }
        }
        return $roles;
    }

    /**
     * Copy privs from other role
     * @param mixed Velis\Acl\Role | int $sourceRole
     */
    public function copyPrivs($sourceRole)
    {
        if ($sourceRole instanceof Role) {
            $sourceRole = $sourceRole->id();
        }

        try {
            self::$_db->startTrans();
                $sourcePrivsSql = 'SELECT rp.app_module_id, rp.priv_acro '
                                . 'FROM acl.role_priv_tab rp '
                                . 'WHERE rp.role_id = :role_id';

                $insertQuery   = "INSERT INTO acl.role_priv_tab (role_id, app_module_id, priv_acro) "
                               . "VALUES (:role_id, :app_module_id, :priv_acro)";

                $insertStmt = self::$_db->prepare($insertQuery);
                $insertParams['role_id'] = $this->id();

            foreach (self::$_db->getAll($sourcePrivsSql, ['role_id' => $sourceRole]) as $priv) {
                $insertParams['app_module_id'] = $priv['app_module_id'];
                $insertParams['priv_acro']     = $priv['priv_acro'];
                $insertStmt->execute($insertParams);
            }
            self::$_db->commit();
        } catch (Exception $ex) {
            self::$_db->rollback();
            throw $ex;
        }
    }
}
