HEX
Server: nginx/1.18.0
System: Linux test-ipsremont 5.4.0-214-generic #234-Ubuntu SMP Fri Mar 14 23:50:27 UTC 2025 x86_64
User: ips (1000)
PHP: 8.0.30
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/html/laravel/app/Models/Notam.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Queue\Jobs\Job;
use Illuminate\Support\Facades\DB;
use JsonSchema\Validator;

/**
 * @property $id integer
 * @property $input string
 * @property $output string
 * @property $chatgpt_response string
 * @property $prompt_tokens string
 * @property $completion_tokens integer
 * @property $total_tokens integer
 * @property $correct_response string
 * @property $comments string
 * @property $prompt_id integer
 * @property $batch_id integer
 * @property $user_id integer
 * @property $request_hash string
 * @property $match_percent int
 * @property $fine_tuning int
 */
class Notam extends Model
{

    protected $table = 'notams';

    public const MAX_TOKENS = 4096;

    public const IN_PROGRESS_STATUS = 'In progress';
    protected $fillable = [
        'input',
        'output',
        'chatgpt_response',
        'prompt_tokens',
        'completion_tokens',
        'total_tokens',
        'correct_response',
        'comments',
        'prompt_id',
        'batch_id',
        'user_id',
        'request_hash',
        'match_percent',
        'fine_tuning',
        'start_date',
        'end_date',
        'new_output',
        'new_correct_response',
        'local_fine_tuning',
        'local_output',
        'local_response',
        'local_correct_response',
        'local_match_percent',
        'local_sent',
        'gpt_rag',
        'model',
        'check_required_gpt',
        'check_required_local',
        'gpt_processing_time',
        'local_processing_time',
        'gpt41_output',
        'gpt41_response',
        'gpt41_correct_response',
        'gpt41_match_percent',
        'gpt41_processing_time',
        'check_required_gpt41',
        'gpt41_prompt_tokens',
        'gpt41_completion_tokens',
        'gpt41_total_tokens',
        'geo_validation_error',
        'gpt_validation_errors',
        'gpt41_validation_errors'
    ];

    public static function getValidResponse($hash)
    {
        $notam = self::query()->where('request_hash', $hash)->whereNotNull('correct_response')->first();

        return $notam->correct_response ?? null;
    }

    public static function hash($input): string
    {
        $input = json_decode($input);
        $text = is_object($input) && property_exists($input, 'Text') ? $input->Text : $input;

        return hash('sha256', json_encode($text, JSON_UNESCAPED_UNICODE));
    }

    public static function sortJson(&$array): void
    {
        foreach ($array as &$value) {
            if (is_array($value)) {
                self::sortJson($value);
            }
        }
        ksort($array);
    }

    public function prepareJsons(): void
    {
        $this->correct_response = self::prepareJson($this->correct_response);
        $this->output = self::prepareJson($this->output);
        $this->local_output = self::prepareJson($this->local_output);
    }

    public static function prepareJson($jsonString): string
    {
        // Removes } from strings like :"something}
        $jsonString = preg_replace('/(Line":")([^{}]*)\},/', '$1$2,', $jsonString);
        $jsonString = str_replace('”', '\"', $jsonString);
        $jsonArray = json_decode($jsonString, true);
        if (is_array($jsonArray)) {
            self::formatJson($jsonArray);
            self::sortJson($jsonArray);

            return json_encode($jsonArray, JSON_UNESCAPED_UNICODE);
        }

        return $jsonString ?? '';
    }


    public static function formatJson(&$jsonArray): void
    {
        foreach ($jsonArray as $key => &$value) {
            switch ($key) {
                case 'SiteIndexA':
                    unset($jsonArray[$key]);
                    $jsonArray['siteIndexA'] = $value;
                    break;
                case 'TYPE':
                    unset($jsonArray[$key]);
                    $jsonArray['type'] = $value;
                    break;
                case 'TITLE':
                    unset($jsonArray[$key]);
                    $jsonArray['title'] = $value;
                    break;
                case 'Traffic':
                    unset($jsonArray[$key]);
                    $jsonArray['traffic'] = $value;
                    break;
                case 'Area':
                    unset($jsonArray[$key]);
                    $jsonArray['area'] = $value;
                    break;
                case 'Text':
                    unset($jsonArray[$key]);
                    $jsonArray['text'] = $value;
                    break;
                case 'CoordinatesLat':
                    unset($jsonArray[$key]);
                    $jsonArray['coordinatesLat'] = $value;
                    break;
                case 'CoordinatesLon':
                    unset($jsonArray[$key]);
                    $jsonArray['coordinatesLon'] = $value;
                    break;
                case 'Radius':
                    unset($jsonArray[$key]);
                    $jsonArray['radius'] = $value;
                    break;
                case 'AltitudeMin':
                    unset($jsonArray[$key]);
                    $jsonArray['altitudeMin'] = $value;
                    break;
                case 'AltitudeMax':
                    unset($jsonArray[$key]);
                    $jsonArray['altitudeMax'] = $value;
                    break;
                case 'N_status':
                    unset($jsonArray[$key]);
                    $jsonArray['N_STATUS'] = $value;
                    break;
            }
        }
    }

    public function getText(): string
    {
        if (substr_count($this->input, '"Text"')) {
            $input = json_decode($this->input);
            $notamText = $input->Text ?? ($input->Text ?? '');
        } else {
            $notamText = $this->input;
        }

        return $notamText;
    }

    public function reachOutput($correctedResponse = false): string|false
    {
        if ($correctedResponse === false) {
            $correctedResponse = $this->output;
        }

        $output = json_decode(self::prepareJson($correctedResponse));

        $output = [
            'CheckNeeded' => $output->CheckNeeded ?? true,
            'A_Line' => $output->A_Line ?? null,
            'B_Line' => $output->B_Line ?? null,
            'C_Line' => $output->C_Line ?? null,
            'D_Line' => $output->D_Line ?? null,
            'E_Line' => $output->E_Line ?? null,
            'F_Line' => $output->F_Line ?? null,
            'G_Line' => $output->G_Line ?? null,
        ];

        foreach ($output as $key => $item) {
            if (empty($item) && !substr_count($key, 'Line') && !substr_count($key, 'CheckNeeded')) {
                unset($output[$key]);
            }
        }

        return json_encode($output, JSON_UNESCAPED_UNICODE);
    }

    public function validate(): Notam
    {
        $this->updateDates();
        $this->prepareJsons();
        $matchPercent = 100;
        if (!$this->correct_response && $this->output) {
            $this->correct_response = $this->output;
        }

        if ($this->correct_response) {
            $validationErrors = $this->validateDataFormat($this->correct_response);
            if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
                $checkNeeded = false;
                unset($validationErrors[0]);
            } else {
                $checkNeeded = true;
            }

            $json = json_decode($this->correct_response);
            if (is_object($json)) {
                if (!property_exists($json, 'CheckNeeded')
                    || $json->CheckNeeded != $checkNeeded) {
                    $json->CheckNeeded = $checkNeeded;
                    $this->correct_response = json_encode($json);
                }
            }
        }

        if ($this->local_correct_response) {
            $validationErrors = $this->validateDataFormat($this->local_correct_response);
            $this->local_fine_tuning = 0;

            if (empty($validationErrors)) {
                $this->local_fine_tuning = 1;
            }
            if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
                $checkNeeded = false;
                unset($validationErrors[0]);
            } else {
                $checkNeeded = true;
            }

            $json = json_decode($this->local_correct_response);
            if (is_object($json)) {
                if (!property_exists($json, 'CheckNeeded')
                    || $json->CheckNeeded != $checkNeeded) {
                    $json->CheckNeeded = $checkNeeded;
                    if ($checkNeeded) {
                        $this->local_fine_tuning = 0;
                    }
                    $this->local_correct_response = json_encode($json);
                }
            }
        }

        if ($this->gpt41_correct_response) {
            // Fine tuning now will be based on GPT 4.1 response
            $validationErrors = $this->validateDataFormat($this->gpt41_correct_response);
            $this->fine_tuning = 0;

            if (empty($validationErrors)) {
                $this->fine_tuning = 1;
            }
            if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
                $checkNeeded = false;
                unset($validationErrors[0]);
            } else {
                $checkNeeded = true;
            }

            $json = json_decode($this->gpt41_correct_response);
            if (is_object($json)) {
                if (!property_exists($json, 'CheckNeeded')
                    || $json->CheckNeeded != $checkNeeded) {
                    $json->CheckNeeded = $checkNeeded;
                    if ($checkNeeded) {
                        $this->fine_tuning = 0;
                    }
                    $this->gpt41_correct_response = json_encode($json);
                }
            }
        }

        $this->compareGptResponses($this->correct_response, $this->gpt41_correct_response);

        Notam::query()->where('id', $this->id)
            ->update([
                'match_percent' => floor($matchPercent),
                'fine_tuning' => $this->fine_tuning ?? 0,
                'local_fine_tuning' => $this->local_fine_tuning ?? 0,
                'check_required_gpt' => substr_count($this->correct_response, '"CheckNeeded":true') > 0,
                'check_required_local' => substr_count($this->local_correct_response, '"CheckNeeded":true') > 0,
                'check_required_gpt41' => substr_count($this->gpt41_correct_response, '"CheckNeeded":true') > 0,
            ]);

        return $this;
    }

    public function compareGptResponses($gptResponse, $additionalResponse)
    {
        $matchPercent = 0;

        try {
            if (!$gptResponse || !$additionalResponse) {
                $this->gpt41_match_percent = $matchPercent;
                $this->comments = 'One of the responses is empty, cannot compare GPT and Local model responses.';
                $this->saveQuietly();

                return $matchPercent;
            }

            $gptResponse = $this->cleanJson($this->prepareJson($this->reachOutput($gptResponse)));
            $additionalModelResponse = $this->cleanJson($this->prepareJson($this->reachOutput($additionalResponse)));

            if ($gptResponse !== $additionalModelResponse) {
                similar_text(
                    $gptResponse,
                    $additionalModelResponse,
                    $matchPercent,
                );
            } else {
                $matchPercent = 100;
            }

            $this->gpt41_match_percent = floor($matchPercent);

//            if ($matchPercent < 90) {
//                $localOutput = json_decode($this->local_correct_response);
//                $localOutput->CheckNeeded = true;
//                $message = json_encode($localOutput);
//                $this->local_correct_response = $message;
//
//                $gptOutput = json_decode($this->correct_response);
//                if ($gptOutput) {
//                    $gptOutput->CheckNeeded = true;
//                    $gptOutput = json_encode($gptOutput);
//                    $this->correct_response = $gptOutput;
//                }
//            }
        } catch (\Exception $e) {
            $this->gpt41_match_percent = 0;
            $this->comments = 'Error comparing GPT and Local model responses: ' . $e->getMessage();
        }

        $this->saveQuietly();

        return floor($matchPercent);
    }

    public function updateDates($prefix = ''): void
    {
        $output = $this->output;
        $correctResponse = $this->correct_response;
        $json = null;
        if ($correctResponse) {
            $json = json_decode($this->correct_response);
        } elseif ($output) {
            $json = json_decode($this->output);
        }

        if ($json === null) {
            return;
        }

        $startDate = null;
        $endDate = null;
        if (!empty($json->B_Line)) {
            $startDate = date('Y-m-d H:i:s', strtotime($json->B_Line));
            if ($startDate) {
                $this->start_date = $startDate;
            }
        }

        if (!empty($json->C_Line) && substr_count($json->C_Line, ':')) {
            $endDate = date('Y-m-d H:i:s', strtotime($json->C_Line));
            if ($endDate) {
                $this->end_date = $endDate;
            }
        }

        $this->saveQuietly();
    }

    public static function cleanJson($jsonString): string
    {
        if (is_string($jsonString)) {
            $jsonString = str_replace(['```json', '```'], '', $jsonString);
            $jsonString = trim($jsonString);

            $data = json_decode($jsonString, true);
        }
        if (is_object($jsonString)) {
            $data = json_decode(json_encode($jsonString), true);
        }
        if ($data) {
            $data = self::cleanOptionalAttributes($data);
        }

        return json_encode($data, JSON_UNESCAPED_UNICODE);
    }

    public static function cleanOptionalAttributes($data): array
    {
        $optionalAttributes = ['Reference', 'CheckNeeded'];

        if (is_array($data)) {
            foreach ($data as $key => $value) {
                if (is_array($value)) {
                    $data[$key] = self::cleanOptionalAttributes($value);
                } elseif (in_array($key, $optionalAttributes)) {
                    unset($data[$key]);
                }
            }
        }

        return $data;
    }

    public function minifyJson($string)
    {
        if (substr_count($string, '”')) {
            $string = str_replace('”', '\"', $string);
        }
        $data = json_decode($string, true);
        $removeAllExcept = ['A_Line', 'B_Line', 'C_Line', 'D_Line', 'E_Line', 'F_Line', 'G_Line'];
        foreach ($data as $key => $value) {
            if (!in_array($key, $removeAllExcept)) {
                unset($data[$key]);
            }
        }

        return json_encode($data, JSON_UNESCAPED_UNICODE);
    }

    public function validateDataFormat($json)
    {
        $iniJson = $json;
        if (is_string($json)) {
            $json = json_decode($json);
        }

        $validator = new Validator();

        $schema = str_replace('const', 'pattern', file_get_contents(base_path('public/schema.json')));
        $schema = json_decode($schema);
        $validator->validate($json, (object) $schema);
        $errors = [];
        if (!$validator->isValid()) {
            foreach ($validator->getErrors() as $error) {
                if (!empty($error['constraint']['params'])) {

                    foreach ($error['constraint']['params'] as $key => $value) {
                        if (is_array($value)) {
                            $value = implode(', ', $value);
                        }
                        $error['message'] = str_ireplace($value, ":$key", $error['message']);
                        $error['constraint']['params'][$key] = __($value, [], 'ru');
                    }
                    $errors[] = $error['property'] . ": " . __(
                            $error['message'],
                            $error['constraint']['params'], 'ru'
                        );
                } else {
                    $errors[] = $error['property'] . ": " . __($error['message'], [], 'ru');
                }
            }
        }

        $errors = self::compareDates($json, $errors);

        if ($this->id) {
            if (!empty($errors)) {
                if ($iniJson == $this->correct_response) {
                    $this->gpt_validation_errors = json_encode($errors);
                }

                if ($iniJson == $this->gpt41_correct_response) {
                    $this->gpt41_validation_errors = json_encode($errors);
                }
            } else {
                if ($iniJson == $this->correct_response) {
                    $this->gpt_validation_errors = '';
                }

                if ($iniJson == $this->gpt41_correct_response) {
                    $this->gpt41_validation_errors = '';
                }
            }

            $this->saveQuietly();
        }

        return $errors;
    }

    public static function compareDates($json, $errors): array
    {
        if (!is_array($json) && !is_object($json)) {
            return $errors;
        }

        foreach ($json as $k => $value) {
            if (is_object($value) || is_array($value)) {
                $errors = self::compareDates($value, $errors);
            } else {
                if ($k === 'startDate') {
                    $startDate = $json->startDate;
                    if (property_exists($json, 'startTime') && gettype($json->startTime) !== "undefined") {
                        $startDate .= ' ' . $json->startTime[0] . $json->startTime[1] . ':' . ($json->startTime[2] ?? 0) . ($json->startTime[3] ?? 0);
                    }
                    $endDate = null;

                    if (property_exists($json, 'endDate') && gettype($json->endDate) !== "undefined" && $json->endDate !== 'PERM') {
                        $endDate = $json->endDate;
                        if (property_exists($json, 'endTime') && gettype($json->endTime) !== "undefined") {
                            $endDate .= ' ' . $json->endTime[0] . $json->endTime[1] . ':' . ($json->endTime[2] ?? 0) . ($json->endTime[3] ?? 0);
                        }
                    } else {
                        if ($json->endDate !== 'PERM') {
                            $errors[] = 'Отсутствует дата окончания';
                            continue;
                        }
                    }

                    if ($startDate && $endDate) {
                        if (strtotime($startDate) > strtotime($endDate)) {
                            $errors[] = 'Время начала действия больше времени окончания: ' . $startDate . ' > ' . $endDate;
                        }
                    }
                } elseif ($k === 'B_Line') {
                    $startDate = self::textToDate($json->B_Line);
                    $endDate = null;
                    if (property_exists($json, 'C_Line') && gettype($json->C_Line) !== 'undefined' && $json->C_Line !== 'PERM') {
                        $endDate = self::textToDate($json->C_Line);
                    } elseif (!property_exists($json, 'C_Line') || $json->C_Line !== 'PERM') {
                        $errors[] = 'Отсутствует дата окончания (C_Line)';
                        continue;
                    }

                    if ($startDate && $endDate) {
                        if (strtotime($startDate) > strtotime($endDate)) {
                            $errors[] = 'Время начала действия NOTAM больше времени окончания: ' . $startDate . ' > ' . $endDate;
                        }
                    }
                }
            }
        }

        return $errors;
    }

    public static function textToDate($t)
    {
        $date = $t;
        if (strlen($t) === 10) {
            $date = '20' . $t[0] . $t[1] . '-' . $t[2] . $t[3] . '-' . $t[4] . $t[5] . ' ' . $t[6] . $t[7] . ":" . $t[8] . $t[9];
        }

        return $date;
    }

    public function processNotamFromQueue()
    {
        try {
            if (!$this->prompt_id) {
                return ['message' => 'Prompt ID is missing'];
            }

            $localModelMatchPercent = 0;
            $prompt = Prompt::query()->find($this->prompt_id);
            $promptText = $prompt->prompt . "\n" . $prompt->json;

            $response = callGptProxi([
                'object_id' => $this->id,
                'input' => $this->getText(),
                'key' => env('OPEN_AI_KEY'),
                'prompt' => $promptText,
                'model' => $prompt->model,
                'temperature' => (float) $prompt->temperature,
                'max_tokens' => self::MAX_TOKENS,
            ], 'notamNew');

            $usage = [
                'prompt_tokens' => 0,
                'completion_tokens' => 0,
                'total_tokens' => 0,
            ];

            // Gpt Model result
            if (property_exists($response, 'choices')) {
                $usage = (array) $response->usage;
                $message = $response->choices[0]->message->content ?? '';
                $message = $this->prepareJson($message);
                $messageData = json_decode($message);

                if (is_object($messageData)) {
                    if (!property_exists($messageData, 'CheckNeeded') || !$messageData->CheckNeeded) {
                        $messageData->CheckNeeded = true; // Force human to check GPT response
                        $message = json_encode($messageData);
                    }
                } else {
                    $response = callGptProxi([
                        'object_id' => $this->id,
                        'input' => $message,
                        'key' => env('OPEN_AI_KEY'),
                        'prompt' => 'Make this json to valid. Dont change data. If needed - you can cut small part of string at the end and close json, to fin 4096 tokens output',
                        'model' => $prompt->model,
                        'temperature' => (float) $prompt->temperature,
                        'max_tokens' => self::MAX_TOKENS,
                    ], 'notamNew');
                    $message = $response->choices[0]->message->content ?? '';
                    if (substr_count($message, '”')) {
                        $message = str_replace('”', '\"', $message);
                    }
                    $message = $this->prepareJson($message);
                    $messageData = json_decode($message);

                    if (!is_object($messageData)) {
                        $this->comments = "Not valid response from OpenAI after second attempt. " . json_encode($response);
                    } elseif (!property_exists($messageData, 'CheckNeeded') || !$messageData->CheckNeeded) {
                        $messageData->CheckNeeded = true;
                        $message = json_encode($messageData);
                    }
                }
            } elseif (property_exists($response, 'output')) {
                // Local model result
                $output = $response->output;
                $message = is_string($output) ? $output : json_encode($output);
                $message = $this->prepareJson($message);
                $messageData = json_decode($message);
                $messageData->CheckNeeded = true;
                $message = json_encode($messageData);
            } else {

                $this->comments = json_encode($response);
                $this->saveQuietly();
            }

            // Gpt Model result
            if (property_exists($response, 'choices')) {
                if (substr_count($prompt->model, '4.1-mini')) {
                    $this->gpt41_output = $message;
                    $this->gpt41_correct_response = $message;
                    $this->gpt41_response = json_encode($response);
                    $this->gpt41_prompt_tokens = $usage['prompt_tokens'];
                    $this->gpt41_completion_tokens = $usage['completion_tokens'];
                    $this->gpt41_total_tokens = $usage['total_tokens'];
                    $this->fine_tuning = 0; // Reset fine tuning flag when new GPT response is received
                    $this->check_required_gpt41 = true; // Set CheckNeeded to true when new GPT response is received
                    // If no errors after GPT response - set flag CheckNeeded to false, but don't change Fine_tuning flag
                    $validationErrors = $this->validateDataFormat($this->gpt41_output);
                    if (!count($validationErrors)
                        || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
                        $messageData->CheckNeeded = false;
                        $message = json_encode($messageData);
                        $this->gpt41_output = $message;
                        $this->gpt41_correct_response = $message; // Fill correct response with GPT model response to make filters work
                    } else {
                        // Temporary disabled to save responses from GPT 4.1
//                        return $validationErrors;
                    }
                } else {
                    $this->output = $message;
                    $this->correct_response = $message;
                    $this->chatgpt_response = json_encode($response);
                    $this->prompt_tokens = $usage['prompt_tokens'];
                    $this->completion_tokens = $usage['completion_tokens'];
                    $this->total_tokens = $usage['total_tokens'];
                    $this->fine_tuning = 0; // Reset fine tuning flag when new GPT response is received
                    $this->check_required_gpt = true; // Set CheckNeeded to true when new GPT response is received
                    // If no errors after GPT response - set flag CheckNeeded to false, but don't change Fine_tuning flag
                    $validationErrors = $this->validateDataFormat($this->output);
                    if (!count($validationErrors)
                        || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
                        $messageData->CheckNeeded = false;
                        $message = json_encode($messageData);
                        $this->output = $message;
                        $this->correct_response = $message; // Fill correct response with GPT model response to make filters work
                    } else {
                        return $validationErrors;
                    }
                }
            } else {
                // Local model response
                $this->local_output = $message;
                $this->local_correct_response = $message; // Fill correct response with Local model response to make filters work
                $this->local_response = json_encode($response); // Full Model response

                if ($message && !substr_count($message, 'process in progress')) {
                    // If no errors after Local response - set flag CheckNeeded to false, but don't change Fine_tuning flag
                    $validationErrors = $this->validateDataFormat($this->local_output);
                    if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
                        $messageData->CheckNeeded = false;
                        $message = json_encode($messageData);
                        $this->local_output = $message;
                        $this->local_correct_response = $message; // Fill correct response with Local model response to make filters work
                    }

                    $this->compareGptResponses($this->correct_response, $this->local_correct_response);
                } else {
                    $this->saveQuietly();

                    return $message;
                }
            }

            $this->updated_at = date('Y-m-d H:i:s');
            $this->saveQuietly(); // calling saveQuietly, because Validate() is called on Save and notam can be marked as Valid when we don't want it
            $this->updateDates();

        } catch (\Exception $exception) {
            echo $exception->getMessage();
            return false;
        }
        return true;
    }

    public static function getStats()
    {
        $prompt = Prompt::query()->where('type', 'notam')->where('is_active', 1)->latest()->first();
        $fineTuningJobs = FineTuningJob::query()->where('prompt_id', $prompt->id)->orderBy('created_at', 'desc')->get();
        $lastJobDate = date('Y-m-d H:i:s');
        if ($fineTuningJobs->first()) {
            $lastJobDate = $fineTuningJobs->first()->created_at;
        }

        $inProgressResponse = json_encode([
            'status' => Notam::IN_PROGRESS_STATUS,
        ]);

        $data = [
            'gptProcessed' => Notam::query()->whereNotNull('chatgpt_response')->count(),
//            'localProcessed' => Notam::query()
//                ->whereNotNull('local_correct_response')
//                ->where('local_correct_response', '<>', $inProgressResponse)
//                ->count(),
            'gpt41Processed' => Notam::query()
                ->whereNotNull('gpt41_correct_response')
                ->where('gpt41_correct_response', '<>', $inProgressResponse)
                ->count(),
            'gptEmpty' => Notam::query()
                ->whereNull('correct_response')
                ->orWhere('correct_response', '')->count(),
//            'localEmpty' => Notam::query()
//                ->whereNull('local_response')
//                ->where('created_at', '>', '2025-12-02')
//                ->count(),
            'gpt41Empty' => Notam::query()
                ->whereNull('gpt41_response')
                ->where('created_at', '>', '2026-04-01')
                ->count(),
            'gptInProgress' => Notam::query()
                ->whereNotNull('correct_response')
                ->where('correct_response', $inProgressResponse)
                ->count(),
//            'localInProgress' => Notam::query()
//                ->whereNotNull('local_correct_response')
//                ->where('local_correct_response', $inProgressResponse)
//                ->count(),
            'gpt41InProgress' => Notam::query()
                ->whereNotNull('gpt41_correct_response')
                ->where('gpt41_correct_response', $inProgressResponse)
                ->count(),
            'checkNeeded' => Notam::query()
                ->where('check_required_gpt', true)
                ->count(),
//            'localCheckNeeded' => Notam::query()
//                ->where('check_required_local', true)
//                ->count(),
            'gpt41CheckNeeded' => Notam::query()
                ->where('check_required_gpt41', true)
                ->count(),
            'actual' => Notam::query()
                ->whereRaw("start_date <= ?", [date('Y-m-d H:i')])
                ->where(function ($query) {
                    $query->whereRaw("end_date >= ?", [date('Y-m-d H:i')])
                        ->orWhereRaw("end_date is NULL");
                })->count(),
            'archived' => Notam::query()
                ->where(function ($query) {
                    $query->whereRaw("start_date > ?", [date('Y-m-d H:i')])
                        ->orWhereRaw("end_date < ?", [date('Y-m-d H:i')]);
                })
                ->whereRaw("end_date IS NOT NULL")
                ->count(),
            'geoJsonInvalid' => Notam::query()
                ->where('geo_validation_error', 1)
                ->count(),
            'gpt41geoJsonInvalid' => Notam::query()
                ->where('geo_validation_error', 1)
                ->count(),
            'validationErrors' => Notam::query()
                ->whereNotNull('gpt_validation_errors')
                ->where('gpt_validation_errors', '<>', '')
                ->count(),
            'validationErrorsGpt41' => Notam::query()
                ->whereNotNull('gpt41_correct_response')
                ->whereNotNull('gpt41_validation_errors')
                ->where('gpt41_validation_errors', '<>', '')
                ->count(),
            'noValidationErrors' => Notam::query()
                ->whereNull('gpt_validation_errors')
                ->orWhere('gpt_validation_errors', '=', '')
                ->count(),
            'noValidationErrorsGpt41' => Notam::query()
                ->whereNotNull('gpt41_correct_response')
                ->where(function ($query) {
                    $query->whereNull('gpt41_validation_errors')
                        ->orWhere('gpt41_validation_errors', '=', '');
                })
                ->count(),
        ];

        $data['noCheckNeeded'] = $data['gptProcessed'] - $data['checkNeeded'] - $data['gptInProgress'];
//        $data['noLocalCheckNeeded'] = $data['localProcessed'] - $data['localCheckNeeded'] - $data['localInProgress'];
        $data['noGpt41CheckNeeded'] = $data['gpt41Processed'] - $data['gpt41CheckNeeded'] - $data['gpt41InProgress'];

        $data['averageGptProcessingTime'] = Notam::query()
            ->whereNotNull('gpt_processing_time')
            ->where('gpt_processing_time', '>', 0)
            ->avg('gpt_processing_time');

        // Format seconds to human readable
        $data['averageGptProcessingTime'] = gmdate("i:s", floor($data['averageGptProcessingTime']));

//        $data['averageLocalProcessingTime'] = Notam::query()
//            ->whereNotNull('local_processing_time')
//            ->where('local_processing_time', '>', 0)
//            ->avg('local_processing_time');
//
//        // Format seconds to human readable
//        $data['averageLocalProcessingTime'] = gmdate("i:s", floor($data['averageLocalProcessingTime']));

        $data['averageGpt41ProcessingTime'] = Notam::query()
            ->whereNotNull('gpt41_processing_time')
            ->where('gpt41_processing_time', '>', 0)
            ->avg('gpt41_processing_time');

        // Format seconds to human readable
        $data['averageGpt41ProcessingTime'] = gmdate("i:s", floor($data['averageGpt41ProcessingTime']));

        $data['jobsCount'] = DB::table('jobs')->where('payload', 'like', '%notam%')->count();
        $data['failedJobsCount'] = DB::table('failed_jobs')->where('payload', 'like', '%notam%')->count();

        return $data;
    }


    public function deleteBadDuplicates()
    {
        if ($this->id && $this->request_hash) {
            Notam::query()
                ->where('id', '!=', $this->id)
                ->where('request_hash', $this->request_hash)
                ->where(function ($query) {
                    $query->where('fine_tuning', 0)
                        ->orWhere('correct_response', 'like', "%\"CheckNeeded\":true%");
                })->delete();
        }
    }

    public function sendToRag($type = 'local')
    {
        $data['model'] = 'local'; // To make call to N8N local LLM proxy

        $input = $this->input;
        if (substr_count($input, '"Text"')) {
            $input = json_decode($input, true);
            $input = $input['Text'];
        }

        if ($type === 'gpt') {
            $endpoint = 'dataToRagGpt';
            if (!$this->correct_response) {
                return true;
            }

            echo $input . ' ' . $this->correct_response . "\n\n";

            $data['input'] = json_encode([
                'input' => $input,
                'output' => $this->correct_response,
            ]);
        } else {
            $endpoint = 'dataToRag';

            if (!$this->local_correct_response) {
                return true;
            }

            echo $input . ' ' . $this->local_correct_response . "\n\n";

            $data['input'] = json_encode([
                'input' => $input,
                'output' => $this->local_correct_response,
            ]);
        }

        $result = callGptProxi($data, $endpoint, 'rag');

        return $result;
    }

    public function calculateGptCost()
    {
        $allCosts = [
            'gpt-5.2' => ['input' => 1.75, 'output' => 14.00],
            'gpt-5.1' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5-mini' => ['input' => 0.25, 'output' => 2.00],
            'gpt-5-nano' => ['input' => 0.05, 'output' => 0.40],
            'gpt-5.2-chat-latest' => ['input' => 1.75, 'output' => 14.00],
            'gpt-5.1-chat-latest' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5-chat-latest' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5.1-codex-max' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5.1-codex' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5-codex' => ['input' => 1.25, 'output' => 10.00],
            'gpt-5.2-pro' => ['input' => 21.00, 'output' => 168.00],
            'gpt-5-pro' => ['input' => 15.00, 'output' => 120.00],
            'gpt-4.1' => ['input' => 2.00, 'output' => 8.00],
            'gpt-4.1-mini' => ['input' => 0.40, 'output' => 1.60],
            'gpt-4.1-nano' => ['input' => 0.10, 'output' => 0.40],
            'gpt-4o' => ['input' => 2.50, 'output' => 10.00],
            'gpt-4o-2024-05-13' => ['input' => 5.00, 'output' => 15.00],
            'gpt-4o-mini' => ['input' => 0.15, 'output' => 0.60],
        ];

        $cost = 0;

        if ($this->model && $this->prompt_tokens !== null && $this->completion_tokens !== null) {
            $modelKey = strtolower($this->model);
            if (!empty($allCosts[$modelKey])) {
                if (array_key_exists($modelKey, $allCosts)) {
                    $costPerMillionInput = $allCosts[$modelKey]['input'];
                    $costPerMillionOutput = $allCosts[$modelKey]['output'];

                    $inputCost = round(($this->prompt_tokens / 1000000) * $costPerMillionInput, 6);
                    $outputCost = round(($this->completion_tokens / 1000000) * $costPerMillionOutput, 6);

                    $cost += $inputCost + $outputCost;
                }
            }
        }

        if ($this->gpt41_prompt_tokens !== null && $this->gpt41_completion_tokens !== null) {
            $modelKey = 'gpt-4.1-mini';
            if (!empty($allCosts[$modelKey])) {
                if (array_key_exists($modelKey, $allCosts)) {
                    $costPerMillionInput = $allCosts[$modelKey]['input'];
                    $costPerMillionOutput = $allCosts[$modelKey]['output'];

                    $inputCost = round(($this->gpt41_prompt_tokens / 1000000) * $costPerMillionInput, 6);
                    $outputCost = round(($this->gpt41_completion_tokens / 1000000) * $costPerMillionOutput, 6);

                    $cost += $inputCost + $outputCost;
                }
            }
        }

        return round($cost, 6);

    }

    public function getMostActualResponse() {
        if ($this->gpt41_correct_response) {
            return $this->gpt41_correct_response;
        }

        if ($this->gpt41_output) {
            return $this->gpt41_output;
        }

        return $this->correct_response ?? $this->output;
    }
}