<?php

namespace App\Src;

use App\Casts\OptionalEncrypted;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;

class Encryption
{
    /**
     * An array of models that have encryption
     *
     * @var array|string[]
     */
    public static array $modelsWithEncryption = [
        '\Mtc\Modules\Members\Models\Member',
        '\Mtc\Modules\Members\Models\MembersAddress',
        '\Mtc\Modules\Members\Models\MemberAttribute',
        '\Mtc\Modules\Members\Models\MemberNote',
        '\Mtc\Plugins\MembersMessaging\Classes\Message',
        '\Mtc\Plugins\NHS\Classes\NHSMember',
        '\Mtc\Plugins\AccountVerifier\Classes\AccountVerifications',
        '\Mtc\Shop\Assessment\Answer',
        '\Mtc\Shop\Order\Note',
        '\Mtc\Plugins\Clinic\Src\Models\PatientFile',
        '\Mtc\Plugins\Clinic\Src\Models\Treatment',
        '\Mtc\Plugins\Clinic\Src\Models\Review',
        '\Mtc\Plugins\Wisebee\Classes\Models\WisebeeConsultation',
        '\Mtc\Plugins\Wisebee\Classes\Models\WisebeeDocument',
        '\Mtc\Plugins\Wisebee\Classes\Models\WisebeeParticipant',
        '\Mtc\Shop\Basket\Address',
        '\Mtc\Shop\Basket\Info',
        '\Mtc\Modules\BlackList\Classes\BlackList',
        '\Mtc\Plugins\NewsletterSignup\Classes\DoubleOptInEmail',
        '\Mtc\Plugins\NewsletterSignup\Classes\MailList',
        '\Mtc\Core\EmailsQueued',
        '\App\Models\EventLog',
        '\Mtc\Shop\Order\Contact',
        '\Mtc\Shop\Order\Address',
        '\Mtc\Shop\Order\Info',
        '\Mtc\Core\AdminUser',
        '\Mtc\Modules\FormBuilder\Classes\Form',
    ];

    /**
     * Creates a hash from a string
     *
     * @param $string
     * @return string
     */
    public static function makeHash($string): string
    {
        return hash('sha512', strtolower(trim($string)) . config('app.key'));
    }

    /**
     * Encrypts specific attributes and objects for a given model.
     *
     * Identifies attributes and objects marked as "encrypted" or "encrypted:object"
     * in the model's casts, decrypts their raw values, and re-encrypts them.
     * Updates the database with the new encrypted values and their corresponding hashes
     * for specific attributes as necessary.
     *
     * @param string $modelName The fully qualified name of the model to process.
     *
     * @return void
     */
    public static function encrypt(string $modelName): void
    {
        $encryptedAttributes = [];
        $model = new $modelName;
        $collection = $model->all();
        foreach ($collection as $key => $item) {
            // Do this on the first row only
            if (empty($key)) {
                foreach ($item->getCasts() as $attribute => $castType) {
                    if ($castType === OptionalEncrypted::class) {
                        // Funnily enough, objects and arrays are encrypted as strings too. Anything else doesn't work.
                        $encryptedAttributes[] = $attribute;
                    }
                }
            }
            $payload = [];
            foreach ($encryptedAttributes as $attribute) {
                $attributeValue = $item->getRawOriginal()[$attribute];
                try {
                    $attributeValue = Crypt::decryptString($attributeValue);
                    $isEncrypted = true;
                } catch (DecryptException) {
                    $isEncrypted = false;
                }
                if ($isEncrypted) {
                    // If it has already been encrypted, skip
                    continue;
                }
                $payload[$attribute] = Crypt::encryptString($attributeValue);
                if (!empty($model::$searchable) && in_array($attribute, $model::$searchable)) {
                    // Create hash for search purposes
                    $payload[$attribute . '_hash'] = Encryption::makeHash($attributeValue);
                }
            }
            if (!empty($payload)) {
                $model::where('id', $item->id)->update($payload);
            }
        }
    }

    /**
     * Update model hashes based on which fields are searchable
     *
     * @param $model
     * @return void
     */
    public static function updateHashes($model): void
    {
        if (empty($model::$searchable)) {
            return;
        }
        foreach ($model::$searchable as $attribute) {
            $model->{$attribute . '_hash'} = self::makeHash($model->getAttribute($attribute));
        }
        // Save quietly to avoid triggering save event again and going into infinite loop
        $model->saveQuietly();
    }

    public static function decrypt(string $modelName): void
    {
        $decryptedAttributes = [];
        $model = new $modelName;
        $collection = $model->all();

        foreach ($collection as $key => $item) {
            // On the first row, determine which attributes are encrypted
            if (empty($key)) {
                foreach ($item->getCasts() as $attribute => $castType) {
                    if ($castType === OptionalEncrypted::class) {
                        $decryptedAttributes[] = $attribute;
                    }
                }
            }

            $payload = [];
            foreach ($decryptedAttributes as $attribute) {
                $attributeValue = $item->getRawOriginal()[$attribute];
                try {
                    $decrypted = Crypt::decryptString($attributeValue);
                    $payload[$attribute] = $decrypted;
                } catch (DecryptException) {
                    // Already decrypted or invalid, skip
                    continue;
                }
            }

            if (!empty($payload)) {
                $model::where('id', $item->id)->update($payload);
            }
        }
    }
}