<?php

namespace Ignite\Core\Entities;

use Exception;
use Ignite\Core\Audit\ParticipantDriver;
use Ignite\Core\Contracts\Entities\User as UserContract;
use Ignite\Core\Contracts\TransactionResourceIdentifier;
use Ignite\Core\Facades\Format;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Auth;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;

/**
 * User
 *
 * @property int user_id
 * @property string username
 * @property string email
 * @property string first
 * @property string last
 * @property string password
 * @property string password_salt
 * @property int status
 * @property int internal
 * @property string remember_token
 * @property string last_login_at
 * @property string created_at
 * @property string updated_at
 * @property \Illuminate\Database\Eloquent\Collection groups
 */
class User extends Authenticatable implements AuditableContract, TransactionResourceIdentifier, UserContract
{
    use Auditable, Notifiable;

    const STAT_INACTIVE = 0;
    const STAT_ACTIVE = 1;
    const STAT_PENDING = 2;
    const TYPE_INTERNAL = 1;
    const TYPE_PARTICIPANT = 0;

    /**
     * @var string
     */
    protected $table = 'core_user';

    /**
     * @var string
     */
    protected $primaryKey = 'user_id';

    /**
     * Custom Audit Driver
     *
     * @var ParticipantDriver
     */
    protected $auditDriver = ParticipantDriver::class;

    /**
     * Attributes to exclude from the Audit.
     *
     * @var array
     */
    protected $auditExclude = [
        'id',
        'password',
        'remember_token',
        'last_login_at',
        'updated_at',
        'created_at',
    ];

    /**
     * The attributes that are not mass assignable.
     *
     * @var array
     */
    protected $guarded = [];

    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'last_login_at' => 'datetime',
    ];

    /**
     * Perform any actions required after the model boots.
     *
     * @return void
     */
    protected static function booted()
    {
        static::addGlobalScope(app(Scopes\UserScope::class));
    }

    public function points()
    {
        return Format::balance($this->transactions->sum('value'));
    }

    /**
     * The field on the user table to identify a transaction resource record.
     *
     * @return string
     */
    public function getTransactionResourceIdentifier()
    {
        return config('core.transaction.resource.identifier', 'email');
    }

    /**
     * Get s list of user types.
     *
     * @return array
     */
    public static function getTypeList()
    {
        return [
            static::TYPE_PARTICIPANT => 'Participant',
            static::TYPE_INTERNAL => 'Internal',
        ];
    }

    /**
     * Get s list of user statuses.
     *
     * @return array
     */
    public static function getStatusList()
    {
        return [
            static::STAT_INACTIVE => 'Inactive',
            static::STAT_PENDING => 'Pending',
            static::STAT_ACTIVE => 'Active',
        ];
    }

    /**
     * Determine whether the user is inactive.
     *
     * @return bool
     */
    public function isInactive()
    {
        return $this->status == static::STAT_INACTIVE;
    }

    /**
     * Determine whether the user is active.
     *
     * @return bool
     */
    public function isActive()
    {
        return $this->status == static::STAT_ACTIVE;
    }

    /**
     * Determine whether the user is pending.
     *
     * @return bool
     */
    public function isPending()
    {
        return $this->status == static::STAT_PENDING;
    }

    /**
     * User groups relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function groups()
    {
        return $this->belongsToMany(Group::class, 'core_user_group', 'user_id', 'group_id')->withTimestamps();
    }

    /**
     * Participant relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function participant()
    {
        return $this->hasOne(Participant::class, 'user_id', 'user_id');
    }

    /**
     * Transaction relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function transactions()
    {
        return $this->hasMany(Transaction::class, 'user_id', 'user_id');
    }

    /**
     * Notes relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function notes()
    {
        return $this->hasMany(Note::class, 'table_primary_id', 'user_id')
            ->whereIn('table_name', ['user', 'participant'])
            ->orderBy('created_at', 'desc');
    }

    /**
     * Get the currently authenticated user id or null.
     *
     * {@inheritdoc}
     */
    public static function resolveId()
    {
        return Auth::check() ? Auth::user()->getAuthIdentifier() : null;
    }

    /**
     * The user's full name.
     *
     * @return string
     */
    public function fullName()
    {
        return $this->first.' '.$this->last;
    }

    /**
     * The identifier for the record in the audit log.
     *
     * @return mixed
     */
    public static function getAuditFriendlyField()
    {
        return ['first', 'last'];
    }

    /**
     * Assign the user to the provided group.
     *
     * @param Group|string $group
     * @return $this
     */
    public function assignToGroup($group)
    {
        if (is_string($group)) {
            // hasGroup() takes strings to be group keys. I feel that'd be best to do here too.
            $group = Group::whereName($group)->firstOrFail();
        }

        if (! $this->relationLoaded('groups')) {
            $this->load('groups');
        }

        if (! $this->groups->contains($group)) {
            // attach() might be better than save(). save() will attach() and also save any updates on the given model.
            // We shouldn't need to make updates to a given Group model here. That is a side effect. I'm leaving this
            // as-is for now because I'm not confident enough that this saving of groups isn't being relied on
            // somewhere.
            $this->groups()->save($group);
        }

        return $this;
    }

    /**
     * Remove the given group from the groups this user has been assigned to.
     *
     * @param Group|string $group key of the group if a string.
     * @return $this
     */
    public function unassignGroup($group)
    {
        if (is_string($group)) {
            $groupKey = $group; // This is only for the error message really :).
            /** @var Group $group */
            $group = Group::query()->firstWhere('key', $groupKey);

            if (! $group) {
                throw new \InvalidArgumentException("Could not remove group from user. Group key '$groupKey' does not exist.");
            }
        }

        $this->groups()->detach($group->getKey());

        return $this;
    }

    /**
     * Determine if a user is associated to a group.
     *
     * @param string|array $group
     * @return bool
     */
    public function hasGroup($group)
    {
        if (is_int($group)) {
            return $this->groups->contains('id', $group);
        }

        if (is_string($group)) {
            return $this->groups->contains('key', $group);
        }

        if ($group instanceof Group) {
            return $this->groups->contains($group);
        }

        if (is_iterable($group)) {
            foreach ($group as $g) {
                if ($this->hasGroup($g)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Is the given group the highest-level group.
     *
     * @param int|string|Group|iterable $group
     * @return bool
     */
    public function isHighestLevelGroup($group): bool
    {
        $highestLevelGroup = $this->getHighestLevelGroup();

        if (! $highestLevelGroup) {
            return false;
        }

        if (is_int($group)) {
            return $highestLevelGroup->id == $group;
        }

        if (is_string($group)) {
            return $highestLevelGroup->key == $group;
        }

        if ($group instanceof Group) {
            return $group->is($highestLevelGroup);
        }

        if (is_iterable($group)) {
            foreach ($group as $g) {
                if ($this->isHighestLevelGroup($g)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * The user's highest level group.
     *
     * @return Group
     */
    public function getHighestLevelGroup()
    {
        return $this->groups->sortBy('level')->first();
    }

    /**
     * Determine whether the user has the given permission.
     *
     * @param Permission|string $permission
     * @return bool
     */
    public function hasPermission($permission)
    {
        /** @var Group $group */
        foreach ($this->groups as $group) {
            if ($group->hasPermission($permission)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get a flat list of permissions across all associated groups.
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getPermissions()
    {
        return $this->groups->flatMap(function ($group) {
            return $group->permissions->keyBy('key')->map(function ($permission) {
                return $permission;
            });
        });
    }

    /**
     * The lowest number level is the highest level privilege. i.e. Level 1 is a super admin.
     *
     * @return int
     * @throws Exception
     */
    public function getLevel()
    {
        $level = (int)$this->groups->min('level');

        if (empty($level)) {
            // TODO: Create a custom Exception class for this case.
            throw new \Exception('User does not have a configured level.');
        }

        return $level;
    }

    /**
     * Scope the query with the last login at.
     *
     * @param Builder $query
     */
    public function scopeWithLastLoginAt(Builder $query)
    {
        $query->addSelect([
            'latest_login_at' => Login::select('created_at')
                ->whereColumn('user_id', 'core_user.user_id')
                ->latest()
                ->take(1),
        ])->withCasts(['latest_login_at' => 'datetime']);
    }
}
