<?php
/*
** Job Arranger Manager
** Copyright (C) 2023 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** Licensed to the Apache Software Foundation (ASF) under one or more
** contributor license agreements. See the NOTICE file distributed with
** this work for additional information regarding copyright ownership.
** The ASF licenses this file to you under the Apache License, Version 2.0
** (the "License"); you may not use this file except in compliance with
** the License. You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
**
**/

namespace App\Utils;

use App\Models\GeneralSettingModel;
use Exception;
use DateTime;
use DateTimeZone;
use Monolog\Logger;

class Util
{
    protected $logger;

    public function __construct()
    {
        $this->logger = Core::logger();
    }
    
    private function aesEncrypt(string $data, string $key): string
    {
        $iv = random_bytes(12);

        $ciphertext = openssl_encrypt(
            $data,
            'aes-256-gcm',
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if ($ciphertext === false) {
            $this->logger->error('AES encryption failed');
        }

        return '2:' . base64_encode($iv . $tag . $ciphertext);
    }

   private function aesDecrypt(string $encrypted, string $key): string
    {
        if (!str_starts_with($encrypted, '2:')) {
            $this->logger->error('Invalid data to decrypt.');
        }

        $data = base64_decode(substr($encrypted, 2), true);

        $iv  = substr($data, 0, 12);
        $tag = substr($data, 12, 16);
        $ciphertext = substr($data, 28);

        $plaintext = openssl_decrypt(
            $ciphertext,
            'aes-256-gcm',
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if ($plaintext === false) {
             $this->logger->error('AES decryption failed');
        }

        return $plaintext;
    }

    /**
     * It checks first character of password.
     *
     * @param string $string run user password
     * @param string $startString
     * @return bool could be true if first char of password is 1, could be false if not
     * @since      function available since version 6.1.0
     */
    public function startsWith($string, $startString)
    {
        $len = strlen($startString);
        return (substr($string, 0, $len) === $startString);
    }

     /**
     * Encrypt password using AES-256-GCM
     *
     * @param string $plaintext
     * @return string encrypted password
     * @since function available since version 7.2.2
     */
        public function encryptPassword(string $plaintext): string
    {
        // AES key (32 bytes)
        $secret = "Oc7odHSwiXU1w3OY8kD2yQzp9GLkYd1Cl821Coqp4uM=";
        $key = hash('sha256', $secret, true);

        // Jaz1 encrypted string
        $jaz1EncryptedString = $this->getPasswordFromString($plaintext);

        // Jaz2 encrypted string
        $jaz2EncryptedString = $this->aesEncrypt($plaintext, $key);

        // Concatenate Jaz1 + Jaz2 
        $combinedString = $jaz1EncryptedString . $jaz2EncryptedString;

        // Combined encryption
        $combinedEncrypted = $this->aesEncrypt($combinedString,$key);

        return  $combinedEncrypted ;
    }

     /**
     * Decrypt password encrypted by AES-256-GCM
     *
     * @param string $encrypted
     * @return string decrypted password
     * @since function available since version 7.2.2
     */
   public function decryptPassword(string $encryptedText): string
    {
        // AES key (32 bytes)
        $secret = "Oc7odHSwiXU1w3OY8kD2yQzp9GLkYd1Cl821Coqp4uM=";
        $key = hash('sha256', $secret, true);

        // Decrypt combined encrypted string
        $combined = $this->aesDecrypt($encryptedText, $key);

        // Extract Jaz2 encrypted string
        $pos = strpos($combined, '2:');
        if ($pos === false) {
            $this->logger->error('AES payload not found in combined data');
            return "";
        }

        $aesEncrypted = substr($combined, $pos);

        // Decrypt Jaz2 encrypted string
        $plaintext = $this->aesDecrypt($aesEncrypted, $key);
        if ($plaintext === false || $plaintext === '') {
            $this->logger->error('Final AES decryption failed');
            return "";
        }

        return $plaintext;
    }

    /**
     * It encrypts password.
     *
     * @param  string $password 
     * @return string encrypts run user password
     * @since      function available since version 6.1.0
     */
    public function getPasswordFromString($str)
    {
        $key = str_split("199907");
        $enc = "1";
        $toX16 = "";

        $j = null;
        $b = null;
        $j = 0;

        $strLen = strlen($str);
        $strArray = str_split($str);
        for ($i = 0; $i < $strLen; $i++) {
            $m = (string) $strArray[$i];
            $n = (string) $key[$j];
            $b = ($m ^ $n);
            //char >> binary >> hex
            $toX16 = bin2hex(iconv('UTF-8', 'ISO-8859-1', $b));
            // if ($b < 16) {
            //     $toX16 = "0" . base_convert($b, 10, 16);
            // } else {
            //     $toX16 = base_convert($b, 10, 16);
            // }
            $enc = (string) $enc . (string) $toX16;

            $j++;
            if ($j == count($key)) $j = 0;
        }

        return $enc;
    }

    /**
     * It decrypts password.
     *
     * @param  string $password 
     * @return string decrypted run user password
     * @since      function available since version 6.1.0
     */
    public function getStringFromPassword($password)
    {
        $key = str_split("199907");
        $dec = "";

        $j = 0;

        $passArray = str_split($password);
        $passLen = count($passArray);
        for ($i = 0; $i < $passLen; $i++) {
            $dd = $password[$i];
            $dec = $dec . ($password[$i] ^ $key[$j]);
            $j++;
            if ($j == count($key)) $j = 0;
        }
        return $dec;
    }

    /**
     * It decrypts password.
     *
     * @param  string $password run user password
     * @return string decoded password 
     *@since      function available since version 6.1.0
     */
    public function getStringFromX16Password($password)
    {
        $enc_code = "";
        $de_code = "";
        $x16 = "";

        $passArray = str_split($password);
        $passLen = count($passArray);
        for ($kk = 0; $kk < $passLen; $kk++) {
            if ($kk == 0) {
                continue;
            } else if (($kk % 2) == 1) {
                $x16 = $passArray[$kk];
            } else {
                $x16 = $x16 . $passArray[$kk];
                $x16 = base_convert($x16, 16, 10);
                $character = json_encode(chr($x16));
                $character = (string)chr($x16);
                $enc_code = $enc_code . $character;
                $x16 = "";
            }
        }
        $de_code = $this->getStringFromPassword($enc_code);

        return $de_code;
    }

    /**
     * It checks basic field validation (present and neither empty nor only white space).
     *
     * @param string $str run user password
     * @return bool could be true if null or white space, could be false for valid password
     * @since      function available since version 6.1.0
     */
    function IsNullOrEmptyString($str)
    {
        return ($str === null || trim($str) === '');
    }

    /**
     * It decrypts password.
     *
     * @param string $passwordText encrypted password
     * @return string decrypted password
     * @since      function available since version 6.1.0
     */
    function getStringFromPass($passwordText)
    {
        if ($this->startsWith($passwordText, '1')) {
            return $this->getStringFromX16Password($passwordText);
        } else {
            return $this->getStringFromPassword($passwordText);
        }
    }

    static function jsonDecode(string $json)
    {
        try {
            $test = json_decode($json, true);
            return $test;
        } catch (Exception $e) {
            return false;
        }
    }
    /**
     * Get datetime by checking ja_2_parameter_table.MANAGER_TIME_SYNC;
     * Use server time if MANAGER_TIME_SYNC value is 1(server time).
     * Use local time(from UI request) if MANAGER_TIME_SYNC value is 2(Local time).
     * If error occurred (on both local and server), use PHP server DateTime.
     * 
     * @return string "YYYYMMDDHHmmss".
     * @since      function available since version 6.1.0
     */
    // public function getDate()
    // {
    //     $generalSettingModel = new GeneralSettingModel();
    //     $req_raw = file_get_contents('php://input');
    //     //get local date from react server.
    //     if (isset($req_raw)) {
    //         $client_date = json_decode($req_raw, true)['date'];
    //         $temp_date = date_create_from_format("YmdHis", $client_date); //check if string is date with desired format.
    //     }
    //     //if request does not contain date or wrong date format, get PHP server datetime.
    //     if (!isset($req_raw) || !$temp_date) {
    //         $temp_date = new DateTime();
    //     }
    //     $local_date = $temp_date->format("YmdHis");
    //     $managerTimeSync = $generalSettingModel->getParameterValue("MANAGER_TIME_SYNC");
    //     //check general_setting->Job Arranger standard time
    //     if ($managerTimeSync == 1) {
    //         //server time
    //         try {
    //             $db = Core::db();
    //             if (DATA_SOURCE_NAME == Constants::DB_MYSQL) {
    //                 $db->query("SELECT CURRENT_TIMESTAMP AS systemtime");
    //             } else {
    //                 $db->query("SELECT NOW() :: timestamp(0) AS systemtime");
    //             }
    //             $db->execute();
    //             $tmp_server_date = $db->resultSet()['0'];
    //             $server_date_obj = date_create_from_format("Y-m-d H:i:s", $tmp_server_date->systemtime);
    //             $serverDate = $server_date_obj->format("YmdHis");
    //             return $serverDate;
    //         } catch (Exception $e) {
    //             //if server error, use local time.
    //             return $local_date;
    //         }
    //     } else {
    //         //local time
    //         return $local_date;
    //     }
    // }

    public function getJobArrangerStandardTime($datetime)
    {
        $generalSettingModel = new GeneralSettingModel();
        $local_date = null;

        // Get local date from React client
        if (!empty($req_raw)) {
            // $client_date = json_decode($req_raw, true)['date'] ?? null;

            if ($datetime) {
                $temp_date = date_create_from_format("YmdHis", $datetime);
                if ($temp_date !== false) {
                    $local_date = $temp_date->format("YmdHis");
                }
            }
        }

        $managerTimeSync = $generalSettingModel->getParameterValue("MANAGER_TIME_SYNC");

        if ($managerTimeSync == 1) {
            // Return server (OS) time in same format: YmdHis
            $serverTimezone = new DateTimeZone(date_default_timezone_get()); // server timezone

            // Create DateTime from the input string assuming it's in server timezone
            $dt = DateTime::createFromFormat('YmdHis', $datetime, $serverTimezone);

            if (!$dt) {
                // fallback to current server time if parsing fails
                return date("YmdHis");
            }

            // Return the formatted datetime string (still server time)
            return $dt->format('YmdHis');
        } else {
            // Return local client time if available
            return $local_date ?? date("YmdHis");
        }
    }

    /**
     * @return int Unix timestamp
     * @since function available since version 6.1.0
     */
    public function getDate()
    {
        $generalSettingModel = new GeneralSettingModel();
        $req_raw = file_get_contents('php://input');
        $temp_date = null;

        // Get local date from React server.
        if (!empty($req_raw)) {
            $decoded = json_decode($req_raw, true);
            if (isset($decoded['date'])) {
                $client_date = $decoded['date'];
                $temp_date = date_create_from_format("YmdHis", $client_date); // Try to parse it
            }
        }

        // If request does not contain date or wrong format, get PHP server datetime.
        if (!$temp_date) {
            $temp_date = new DateTime();
        }

        $managerTimeSync = $generalSettingModel->getParameterValue("MANAGER_TIME_SYNC");

        if ($managerTimeSync == 1) {
            // Server time
            try {
                $db = Core::db();
                if (DATA_SOURCE_NAME == Constants::DB_MYSQL) {
                    $db->query("SELECT CURRENT_TIMESTAMP AS systemtime");
                } else {
                    $db->query("SELECT NOW() :: timestamp(0) AS systemtime");
                }
                $db->execute();
                $tmp_server_date = $db->resultSet()[0];
                $server_date_obj = date_create_from_format("Y-m-d H:i:s", $tmp_server_date->systemtime);
                return $server_date_obj->getTimestamp();
            } catch (Exception $e) {
                // If server error, use local time.
                return $temp_date->getTimestamp();
            }
        } else {
            // Local time
            return $temp_date->getTimestamp();
        }
    }

    /**
     * create a response json by given parameters.
     * @param string $type  Response Message Type.
     * @param string $detail    Detail of the response.
     * @param string $ObjectId  ID of resulting response object.
     * @param array $returnItemData related object list.
     * @return array    return a json message response.
     */
    public function createResponseJson($detail = "", $ObjectId = "", $returnItemData = [])
    {
        if ($detail == null || $detail == "") {
            $detail == "lab-server-error";
        }
        $detailTxt = Constants::MESSAGE[$detail];
        if ($detailTxt == null) {
            $detailTxt = "";
        }
        $data[Constants::AJAX_MESSAGE_DETAIL] =  $detail;
        $data[Constants::AJAX_MESSAGE_OBJECTID] = $ObjectId;
        $data[Constants::AJAX_MESSAGE_DETAIL_TXT] = $detailTxt;
        if ($returnItemData != null) {
            $data[Constants::AJAX_RETURN_ITEM_DATA] = (array)$returnItemData;
        }
        return (array)$data;
    }

    /**
     * create a common api response json by given parameters.
     * @param string $type  Response Type.
     * @param string $detail    Detail of the response.
     * @return array    return a json message response.
     */
    static function response($type, $detail = [])
    {
        return json_encode([
            Constants::API_RESPONSE_TYPE => $type,
            Constants::API_RESPONSE_DETAIL => $detail
        ]);
    }

    static function getLockObjectType(int $objectType): String
    {
        switch ($objectType) {
            case Constants::OBJECT_TYPE_CALENDAR:
                $objectTypeString = Constants::OBJECT_TYPE_CALENDAR_STRING;
                break;
            case Constants::OBJECT_TYPE_SCHEDULE:
                $objectTypeString = Constants::OBJECT_TYPE_SCHEDULE_STRING;
                break;
            case Constants::OBJECT_TYPE_FILTER:
                $objectTypeString = Constants::OBJECT_TYPE_FILTER_STRING;
                break;
            case Constants::OBJECT_TYPE_JOBNET:
                $objectTypeString = Constants::OBJECT_TYPE_JOBNET_STRING;
                break;
        }
        return $objectTypeString;
    }
    public function transformVariable(array $variable, string $variable_type): array
    {
        $result = [];
        // sort keys alphabetically
        ksort($variable);
        foreach ($variable as $key => $value) {
            $result[] = [
                'value_name' => $key,
                $variable_type => (string) $value,
            ];
        }

        return $result;
    }

    /**
     * Convert provided start time (YYYYMMDDHHMM) into Unix timestamp
     * @param string|int|null $startTime
     * @return int Unix timestamp
     */
    public function convertUnixTime($startTime)
    {
        if ($startTime === null || $startTime === '') {
            return 0; 
        }
        $startTime = (string) $startTime;

        $dateTime = DateTime::createFromFormat('YmdHi', $startTime);

        if ($dateTime === false) {
            return 0;
        }
        return $dateTime->getTimestamp();
    }

    public function getUnixTimeFromTimeZone($timezone, $date)
    {
        $generalSettingModel = new GeneralSettingModel();
        $temp_date = null;
    
        $defaultTimezone = new \DateTimeZone(date_default_timezone_get());
    
        if ($date) {
            try {
                if ($timezone && in_array($timezone, \DateTimeZone::listIdentifiers())) {
                    $timezone = new \DateTimeZone($timezone);
                } else {
                    $timezone = $defaultTimezone;
                }
    
                $temp_date = \DateTime::createFromFormat("YmdHis", $date, $timezone);
    
                if (!$temp_date) {
                    $temp_date = new \DateTime("now", $defaultTimezone);
                }
            } catch (\Exception $e) {
                $temp_date = new \DateTime("now", $defaultTimezone);
            }
        }
    
        if (!$temp_date) {
            $temp_date = new \DateTime("now", $defaultTimezone);
        }
    
        // rest of your DB sync logic...
        return $temp_date->getTimestamp();
    }
}


