<?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\Model;

use Buckhill\U2F\U2FKey;
use function Buckhill\U2F\u2f;
use Illuminate\Database\Eloquent\Model;
use AuthStack\TwoFactor\Model\U2F\Authentication;
use AuthStack\TwoFactor\Verifier\VerifierInterface;

/**
 * Class MFAKey
 * @package MFAStack\Models
 */
class MFAKey extends Model
{
    protected $table = 'mfa_key';
    protected $fillable = ['mode_id', 'public_id', 'title', 'metadata', 'created_at', 'updated_at'];
    public $timestamps = true;

    // Convert metadata from JSON to array immediately
    protected $casts = [
        'metadata' => 'array'
    ];

    protected $verifier = null;

    /**
     * @param $value
     */
    public function setMetadataAttribute($value)
    {
        $this->attributes['metadata'] = json_encode($value, JSON_UNESCAPED_SLASHES);
    }

    /**
     * @return bool
     */
    public function isU2F()
    {
        return $this->getAttribute('mode_id') == MFAMode::MODE_U2F;
    }

    /**
     * Creates a U2F challenge
     *
     * @return array
     */
    public function challenge()
    {
        if(!$this->isU2F())
        {
            throw new \LogicException("Not a U2F key, unable to create the challenge");
        }

        $u2f = U2FKey::fromArray($this->metadata);

        $u2f->createChallenge();

        $challenge = Authentication::create([
            'key_id'    => $this->id,
            'challenge' => $u2f->getChallenge(),
            'version'   => $u2f->getVersion()
        ]);

        return [
            'request_id'        => $challenge->id,
            'challenge'         => $challenge->challenge,
            'version'           => $challenge->version,
            'keyHandle'         => $u2f->getKeyHandle(),
            'appId'             => $u2f->getAppId()
        ];
    }

    /**
     * @param Authentication $challenge
     * @param array $responseData
     * @return bool
     */
    public function authenticate(Authentication $challenge, array $responseData)
    {
        $u2f = U2FKey::fromArray(array_merge($this->metadata,
            [
                'challenge' => $challenge->challenge
            ]
        ));

        $result = u2f()->authenticate()->response($responseData, $u2f);

        // Save the new counter to database
        $this->metadata = array_merge($this->metadata, [
            'counter' => $result->getCounter()
        ]);

        $this->used_at = $this->freshTimestampString();

        return $this->save();
    }

    /**
     * @param $key_id
     *
     * @return MFAKey
     */
    public static function loadByKeyId($key_id)
    {
        $model = static::where(is_numeric($key_id) ? 'id' : 'public_id', '=', $key_id)->firstOrFail();

        return $model;
    }

    /**
     * @param string $key
     * @param null   $default
     *
     * @return string
     */
    public function fromMetadata($key = '', $default = null)
    {
        return isset($this->metadata[$key]) ? $this->metadata[$key] : $default;
    }

    /**
     * @param $otp
     * @return bool
     */
    public function verify($otp)
    {
        $verifier = $this->getVerifier();

        if(is_null($verifier))
        {
            return false;
        }

        $this->verifier = $verifier;

        return $verifier->verify($otp);
    }

    /**
     * Updates security key information after verifying the OTP
     *
     * @return bool
     */
    public function synchronize()
    {
        $verifier = $this->verifier;

        if(!$verifier instanceof VerifierInterface)
        {
            throw new \BadMethodCallException("No VerifierInterface found. Run verify() first before invoking synchronize()");
        }

        $this->metadata = array_merge($this->metadata, $verifier->getResult());
        $this->used_at = $this->freshTimestampString();

        return $this->save();
    }

    /**
     * @param bool $as_string
     * @return array
     */
    public function getFailureReason($as_string = false)
    {
        $verifier = $this->verifier;

        if(!$verifier instanceof VerifierInterface)
        {
            throw new \BadMethodCallException("No VerifierInterface found. Run verify() first before invoking synchronize()");
        }

        if($as_string)
        {
            return $verifier->getErrors()[0];
        }

        return $verifier->getErrors();
    }

    /**
     * @return null|VerifierInterface
     */
    protected function getVerifier()
    {
        $result = null;

        switch($this->mode_id)
        {
            case MFAMode::MODE_RFC4226:
                $result = 'RFC4226';
                break;

            case MFAMode::MODE_RFC6238:
                $result = 'RFC6238';
                break;

            case MFAMode::MODE_YUBIKEY:
                $result = 'Yubikey';
                break;

            case MFAMode::MODE_YUBIKEY_OATH:
                $result = 'YubikeyOATH';
                break;
        }

        if(!is_null($result))
        {
            $class = sprintf("AuthStack\\TwoFactor\\Verifier\\Implementation\\%s", $result);

            $result = new $class($this->metadata);
        }

        return $result;
    }
}