<?php

/*
 * This file is part of the AuthStack project.
 *
 * (c) Buckhill Ltd <development@buckhill.co.uk>
 *
 * This source file is subject to the Buckhill license that is bundled
 * with this source code in the file LICENSE.
 */

namespace AuthStack\TwoFactor\Collection;

use AuthStack\TwoFactor\Model\U2F\Authentication;
use Illuminate\Support\Collection;
use AuthStack\TwoFactor\Model\MFAKey;
use AuthStack\TwoFactor\Model\MFAMode;
use AuthStack\TwoFactor\Model\SecurityKey;
use AuthStack\TwoFactor\Model\U2F\Enrollment;
use AuthStack\TwoFactor\Model\ViewSecurityKey;

class KeyCollection implements KeyCollectionInterface
{
    protected $collection;
    protected $user_id;
    protected $otp_verification_msg;
    protected $last_error = '';

    /**
     * Finds all keys that belong to user identified by $id and loads them into the collection
     *
     * @param $id
     * @return static
     */
    public static function loadForUser($id)
    {
        $ids = SecurityKey::where("user_id", $id)->where("confirmed", 1)->orderBy('id', 'ASC')->pluck('key_id')->toArray();

        $mfakeys = MFAKey::whereIn('id', $ids)->get();

        $me = new static;

        $me->user_id = $id;

        return $me->setCollection(collect($mfakeys));
    }

    /**
     * @param array $key
     * @param int $confirmed
     * @return bool|static
     */
    public function add(array $key, $confirmed = 0)
    {
        $result = false;

        try
        {
            $mfakey = MFAKey::create($key);

            SecurityKey::create([
                'user_id'   => $this->user_id,
                'key_id'    => $mfakey->id,
                'confirmed' => $confirmed
            ]);

            $result = $mfakey;

            $this->getKeyCollection()->push($mfakey);
        }
        catch(\Exception $e)
        {
            $this->last_error = $e->getMessage();
        }

        return $result;
    }

    /**
     * @param $key_id
     * @param $user_id
     * @return bool
     */
    public function enable($key_id, $user_id)
    {
        $result = false;

        try
        {
            $model = SecurityKey::where("user_id", $user_id)->where("key_id", $key_id)->firstOrFail();

            $model->confirmed = 1;

            $result = $model->save();
        }
        catch(\Exception $e)
        {}

        return $result;
    }

    /**
     * @param $id
     * @param array $data
     * @return bool
     */
    public function update($id, array $data)
    {
        $key = $this->getKeyCollection()->where('id', $id)->first();

        if(!$key instanceof MFAKey)
        {
            $this->last_error = 'No such key in collection. ID = ' . $id;

            return false;
        }

        foreach($data as $column => $value)
        {
            $key->setAttribute($column, $value);
        }

        return $key->save();
    }

    /**
     * @param $app_id
     * @param $title
     * @return array
     */
    public function enroll($app_id, $title)
    {
        return Enrollment::enroll($app_id, $title);
    }

    /**
     * @param $challenge_id
     * @param array $data
     * @return array
     */
    public function register($challenge_id, array $data)
    {
        try
        {
            $key = Enrollment::register($challenge_id, $data);

            $result = [
                'success' => $this->add($key, 1)
            ];
        }
        catch(\Exception $e)
        {
            $result = [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }

        return $result;
    }

    /**
     * @return bool|array
     */
    public function challenge(callable $transform = null)
    {
        $u2f = $this->getU2FKeys();

        if(!$u2f->count())
        {
            return false;
        }

        $challenges = $u2f->map(function(MFAKey $key) use ($transform)
            {
                $challenge = $key->challenge();

                if(is_callable($transform))
                {
                    $challenge = $transform($challenge);
                }

                return $challenge;
            })
            ->toArray();


        $result = [];

        if(!count($challenges)) return $result;

        foreach($challenges as $key => $challenge)
        {
            $result[] = $challenge;
        }

        return $result;
    }

    /**
     * @param $challenge_id
     * @param array $responseData
     * @return bool
     */
    public function authenticate($challenge_id, array $responseData)
    {
        $result = false;

        try
        {
            $auth   = Authentication::findOrFail($challenge_id);
            $key    = $this->getKeyCollection()->where('id', $auth->key_id)->first();

            if(!$key instanceof MFAKey)
            {
                $this->last_error = 'Specified key is not within the key collection, unable to verify U2F auth response';
            }
            else
            {
                $result = $key->authenticate($auth, $responseData);
            }
        }
        catch(\Exception $e)
        {
            $this->last_error = $e->getMessage();
        }

        return $result;
    }

    /**
     * Removes the key from collection and deletes it from database. Only works with keys that belong to currently
     * logged in user
     *
     * @param $id
     * @return bool|null
     */
    public function remove($id)
    {
        $key = $this->getKeyCollection()->where('id', $id)->first();

        if(!$key instanceof MFAKey)
        {
            $this->last_error = 'No such key in collection. ID = ' . $id;

            return false;
        }

        // Remove the key from collection and return filtered collection
        $collection = $this->getKeyCollection()->reject(function($value) use (&$key)
        {
            return (int)$value->id == (int)$key->id;
        });

        $this->setCollection($collection);

        return $key->delete();
    }

    /**
     * @param $otp
     * @return bool
     */
    public function verifyOTP($otp)
    {
        if(!$this->hasOTP())
        {
            $this->setOTPError('User has no security keys that produce OTPs associated with their account, skipping the check.');

            return false;
        }

        $keys = $this->getOTPKeys();

        $result = false;

        foreach($keys as $key)
        {
            /** @var $key MFAKey */

            $result = $key->verify($otp);

            if($result)
            {
                $key->synchronize();

                break;
            }
        }

        return $result;
    }

    /**
     * @return Collection
     */
    public function getKeyCollection()
    {
        return $this->collection;
    }

    /**
     * @return int
     */
    public function count()
    {
        return $this->getKeyCollection()->count();
    }

    /**
     * @return bool
     */
    public function hasU2F()
    {
        return (bool)$this->getKeyCollection()->where('mode_id', MFAMode::MODE_U2F)->count();
    }

    /**
     * @return bool
     */
    public function hasOTP()
    {
        return (bool)$this->getOTPKeys()->count();
    }

    /**
     * @return Collection
     */
    public function getOTPKeys()
    {
        return $this
            ->getKeyCollection()
            ->whereIn('mode_id', [
                MFAMode::MODE_RFC4226,
                MFAMode::MODE_RFC6238,
                MFAMode::MODE_YUBIKEY,
                MFAMode::MODE_YUBIKEY_OATH
            ]);
    }

    /**
     * @return Collection
     */
    public function getU2FKeys()
    {
        return $this
            ->getKeyCollection()
            ->where('mode_id', MFAMode::MODE_U2F)
            ;
    }

    /**
     * @return array
     */
    public function toArray()
    {
        return $this->getKeyCollection()->toArray();
    }


    /**
     * @param null $user_id
     * @return array
     */
    public function getKeyList($user_id = null)
    {
        $user_id = is_null($user_id) ? $this->user_id : $user_id;

        return ViewSecurityKey::forUser($user_id);
    }


    /**
     * @return string
     */
    public function getLastError()
    {
        return $this->last_error;
    }

    /**
     * @param Collection $collection
     * @return $this
     */
    protected function setCollection(Collection $collection)
    {
        $this->collection = $collection;

        return $this;
    }

    /**
     * @param $msg
     * @return $this
     */
    protected function setOTPError($msg)
    {
        $this->otp_verification_msg = $msg;

        return $this;
    }
}