<?php

namespace AuthStack\Driver\Model;

use Illuminate\Http\Request;
use Buckhill\RFC4226\OTP as HOTP;
use Buckhill\RFC6238\OTP as TOTP;
use Illuminate\Database\Eloquent\Model;
use AuthStack\Driver\Errors\DecodingError;
use AuthStack\Driver\Errors\EncodingError;
use Buckhill\Yubico\Modes\Yubikey\Yubikey;
use Buckhill\Yubico\Modes\OATH\YubikeyOATH;
use Buckhill\Yubico\Exceptions\InvalidOTPException;
use Buckhill\Yubico\Modes\Yubikey\OTP as YubikeyOTP;
use Buckhill\Yubico\Modes\OATH\OTP as YubikeyOATHOTP;

/**
 * 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'
    ];


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


    /**
     * @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
     * @throws DecodingError
     */
    public function fromMetadata($key = '', $default = null)
    {
        return isset($this->metadata[$key]) ? $this->metadata[$key] : $default;
    }


    /**
     * @param $otp
     *
     * @return bool
     * @throws \Exception
     */
    public function verify($otp)
    {
        switch($this->mode_id)
        {
            case MFAMode::MODE_YUBIKEY:

                return $this->verifyYubikey($otp);

                break;

            case MFAMode::MODE_YUBIKEY_OATH:

                return $this->verifyYubikeyOATH($otp);

                break;

            case MFAMode::MODE_RFC4226:

                return $this->verifyRFC4226($otp);

                break;

            case MFAMode::MODE_RFC6238:

                return $this->verifyRFC6238($otp);

                break;

            case MFAMode::MODE_U2F:
            default:
                throw new \Exception("Verification doesn't work for the selected mode.");
                break;
        }
    }


    /**
     * @param $otp
     *
     * @return bool
     */
    public function verifyYubikey($otp)
    {
        $otp        = new YubikeyOTP($otp);
        $yubikey    = Yubikey::fromArray($this->metadata);
        $decrypted = $otp->decrypt($yubikey);

        // Validate
        $decrypted->validate();

        // If validation passes, we're going to reach this line
        $metadata = array_merge($this->metadata, [
            'session_counter'   => $decrypted->getSessionCounter() + 1,
            'use_counter'       => $decrypted->getUseCounter()
        ]);

        $this->metadata = $metadata;

        return $this->save();
    }


    /**
     * @param $otp
     *
     * @return bool
     * @throws DecodingError
     * @throws EncodingError
     */
    public function verifyYubikeyOATH($otp)
    {
        $otp            = new YubikeyOATHOTP($otp);
        $yubikey        = YubikeyOATH::fromArray(array_merge(['identifier' => $this->public_id], $this->metadata));
        $result         = $yubikey->verify($otp);

        $metadata       = $this->metadata;
        $metadata['counter'] = $result + 1;

        $this->metadata = $metadata;

        return $this->save();
    }


    /**
     * @param $otp
     *
     * @return bool
     * @throws DecodingError
     * @throws EncodingError
     * @throws InvalidOTPException
     */
    public function verifyRFC4226($otp)
    {
        $metadata   = $this->metadata;
        $secret     = $metadata['secret'];
        $counter    = (int)$metadata['counter'];
        $auth       = new HOTP($secret);

        $auth->setConversionMethod(function($secret) {
            return $secret;
        });

        $result = $auth->where((int)$otp, $counter, 1000);

        if(false === $result)
        {
            throw new InvalidOTPException("Invalid OTP.");
        }

        $metadata['counter'] = (int)$result + 1;

        $this->metadata = $metadata;

        return $this->save();
    }


    /**
     * @param $otp
     *
     * @return bool
     * @throws DecodingError
     * @throws InvalidOTPException
     */
    public function verifyRFC6238($otp)
    {
        $metadata   = $this->metadata;
        $secret     = $metadata['secret'];
        $auth       = new TOTP($secret);

        $auth->setConversionMethod(function($secret){
            return $secret;
        });

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

        if(false === $result)
        {
            throw new InvalidOTPException("Invalid OTP.");
        }

        return true;
    }


    /**
     * @param $str
     *
     * @return array
     * @throws DecodingError
     */
    protected function JSONDecode($str)
    {
        $json = @json_decode($str, true);

        if(json_last_error() !== JSON_ERROR_NONE)
        {
            throw new DecodingError(json_last_error_msg());
        }

        return $json;
    }


    /**
     * @param array $data
     * @param int   $options
     * @param int   $depth
     *
     * @return string
     * @throws EncodingError
     */
    protected function JSONEncode(array $data, $options = 0, $depth = 512)
    {
        $encoded = @json_encode($data, $options, $depth);

        if(false === $encoded)
        {
            throw new EncodingError(json_last_error_msg());
        }

        return $encoded;
    }
}