<?php

declare(strict_types=1);

namespace App\Modules\Administration\Services;

use App\Modules\Administration\Repositories\EmployeeRankPromotionRepository;
use RuntimeException;
use System\Database\QueryBuilder;

final class EmployeeRankPromotionService
{
    public function __construct(
        private EmployeeRankPromotionRepository $promotions,
        private EmployeeService $employees,
        private QueryBuilder $query
    ) {
    }

    public function byEmployee(
        int $employeeId
    ): array {
        return $this->promotions
            ->byEmployee($employeeId);
    }

    public function find(
        int $id
    ): ?array {
        return $this->promotions
            ->find($id);
    }

    public function ranks(): array
    {
        return $this->query
            ->table('ranks')
            ->where(
                'status',
                1
            )
            ->orderBy(
                'rank_name',
                'ASC'
            )
            ->get();
    }

    public function create(
        int $employeeId,
        array $data,
        ?array $file = null
    ): void {
        $employee =
            $this->employees->find(
                $employeeId
            );

        if ($employee === null) {
            throw new RuntimeException(
                'Employee not found.'
            );
        }

        $newRankId = (int) (
            $data['new_rank_id']
            ?? 0
        );

        if ($newRankId < 1) {
            throw new RuntimeException(
                'New rank is required.'
            );
        }

        $newRank =
            $this->findRank(
                $newRankId
            );

        if ($newRank === null) {
            throw new RuntimeException(
                'Selected new rank was not found.'
            );
        }

        $previousRankId = (int) (
            $data['previous_rank_id']
            ?? 0
        );

        $previousRank = null;

        if ($previousRankId > 0) {
            $previousRank =
                $this->findRank(
                    $previousRankId
                );

            if ($previousRank === null) {
                throw new RuntimeException(
                    'Selected previous rank was not found.'
                );
            }
        }

        $promotionType = trim(
            (string) (
                $data['promotion_type']
                ?? ''
            )
        );

        if ($promotionType === '') {
            throw new RuntimeException(
                'Promotion type is required.'
            );
        }

        $effectiveDate = trim(
            (string) (
                $data['effective_date']
                ?? ''
            )
        );

        if ($effectiveDate === '') {
            throw new RuntimeException(
                'Effective date is required.'
            );
        }

        if (
            $previousRankId > 0
            && $previousRankId === $newRankId
        ) {
            throw new RuntimeException(
                'Previous rank and new rank cannot be the same.'
            );
        }

        $employeeCode = trim(
            (string) (
                $employee['employee_id']
                ?? ''
            )
        );

        if ($employeeCode === '') {
            throw new RuntimeException(
                'Employee ID code is missing.'
            );
        }

        $document = [
            'document_file_path' =>
                null,

            'document_original_name' =>
                null,

            'document_mime_type' =>
                null,

            'document_file_size' =>
                null
        ];

        if (
            $file !== null
            && (int) (
                $file['error']
                ?? UPLOAD_ERR_NO_FILE
            ) !== UPLOAD_ERR_NO_FILE
        ) {
            $document =
                $this->uploadDocument(
                    $file,
                    $employeeCode
                );
        }

        $now =
            date('Y-m-d H:i:s');

        $this->promotions->create([
            'employee_id' =>
                $employeeId,

            'previous_rank_id' =>
                $previousRankId > 0
                    ? $previousRankId
                    : null,

            'new_rank_id' =>
                $newRankId,

            'previous_rank_name' =>
                $previousRank !== null
                    ? (string) (
                        $previousRank['rank_name']
                        ?? ''
                    )
                    : null,

            'new_rank_name' =>
                (string) (
                    $newRank['rank_name']
                    ?? ''
                ),

            'promotion_type' =>
                $promotionType,

            'effective_date' =>
                $effectiveDate,

            'order_number' =>
                $this->nullableString(
                    $data['order_number']
                    ?? null
                ),

            'promotion_reason' =>
                $this->nullableString(
                    $data['promotion_reason']
                    ?? null
                ),

            'notes' =>
                $this->nullableString(
                    $data['notes']
                    ?? null
                ),

            'document_file_path' =>
                $document[
                    'document_file_path'
                ],

            'document_original_name' =>
                $document[
                    'document_original_name'
                ],

            'document_mime_type' =>
                $document[
                    'document_mime_type'
                ],

            'document_file_size' =>
                $document[
                    'document_file_size'
                ],

            'status' =>
                1,

            'created_at' =>
                $now,

            'updated_at' =>
                $now
        ]);
    }
    
public function update(
    int $id,
    array $data,
    ?array $file = null
): void {
    $promotion =
        $this->promotions->find($id);

    if ($promotion === null) {
        throw new RuntimeException(
            'Promotion record not found.'
        );
    }

    $newRankId = (int) (
        $data['new_rank_id']
        ?? 0
    );

    if ($newRankId < 1) {
        throw new RuntimeException(
            'New rank is required.'
        );
    }

    $newRank =
        $this->findRank($newRankId);

    if ($newRank === null) {
        throw new RuntimeException(
            'Selected new rank was not found.'
        );
    }

    $previousRankId = (int) (
        $data['previous_rank_id']
        ?? 0
    );

    $previousRank = null;

    if ($previousRankId > 0) {
        $previousRank =
            $this->findRank(
                $previousRankId
            );

        if ($previousRank === null) {
            throw new RuntimeException(
                'Selected previous rank was not found.'
            );
        }
    }

    if (
        $previousRankId > 0
        && $previousRankId === $newRankId
    ) {
        throw new RuntimeException(
            'Previous rank and new rank cannot be the same.'
        );
    }

    $promotionType = trim(
        (string) (
            $data['promotion_type']
            ?? ''
        )
    );

    if ($promotionType === '') {
        throw new RuntimeException(
            'Promotion type is required.'
        );
    }

    $effectiveDate = trim(
        (string) (
            $data['effective_date']
            ?? ''
        )
    );

    if ($effectiveDate === '') {
        throw new RuntimeException(
            'Effective date is required.'
        );
    }

    /*
    |--------------------------------------------------------------------------
    | Preserve existing document
    |--------------------------------------------------------------------------
    */

    $document = [
        'document_file_path' =>
            $promotion['document_file_path']
            ?? null,

        'document_original_name' =>
            $promotion['document_original_name']
            ?? null,

        'document_mime_type' =>
            $promotion['document_mime_type']
            ?? null,

        'document_file_size' =>
            $promotion['document_file_size']
            ?? null
    ];

    /*
    |--------------------------------------------------------------------------
    | Replace document only when new file is uploaded
    |--------------------------------------------------------------------------
    */

    if (
        $file !== null
        && (int) (
            $file['error']
            ?? UPLOAD_ERR_NO_FILE
        ) !== UPLOAD_ERR_NO_FILE
    ) {
        $employee =
            $this->employees->find(
                (int) $promotion['employee_id']
            );

        if ($employee === null) {
            throw new RuntimeException(
                'Employee not found.'
            );
        }

        $employeeCode = trim(
            (string) (
                $employee['employee_id']
                ?? ''
            )
        );

        if ($employeeCode === '') {
            throw new RuntimeException(
                'Employee ID code is missing.'
            );
        }

        /*
        | Save replacement first.
        */

        $newDocument =
            $this->uploadDocument(
                $file,
                $employeeCode
            );

        /*
        | Remove previous physical file only
        | after replacement upload succeeds.
        */

        $oldRelativePath = (string) (
            $promotion['document_file_path']
            ?? ''
        );

        if ($oldRelativePath !== '') {
            $oldFile =
                PUBLIC_PATH
                . '/'
                . ltrim(
                    $oldRelativePath,
                    '/'
                );

            if (
                is_file($oldFile)
                && is_writable($oldFile)
            ) {
                unlink($oldFile);
            }
        }

        $document = $newDocument;
    }

    /*
    |--------------------------------------------------------------------------
    | Rank name snapshots
    |--------------------------------------------------------------------------
    |
    | IMPORTANT:
    | If your ranks table uses rank_name instead of name,
    | replace ['name'] below with ['rank_name'].
    */

    $previousRankName = null;

    if ($previousRank !== null) {
        $previousRankName = (string) (
            $previousRank['name']
            ?? $previousRank['rank_name']
            ?? ''
        );
    }

    $newRankName = (string) (
        $newRank['name']
        ?? $newRank['rank_name']
        ?? ''
    );

    $this->promotions->update(
        $id,
        [
            'previous_rank_id' =>
                $previousRankId > 0
                    ? $previousRankId
                    : null,

            'new_rank_id' =>
                $newRankId,

            'previous_rank_name' =>
                $previousRankName,

            'new_rank_name' =>
                $newRankName,

            'promotion_type' =>
                $promotionType,

            'effective_date' =>
                $effectiveDate,

            'order_number' =>
                $this->nullableString(
                    $data['order_number']
                    ?? null
                ),

            'promotion_reason' =>
                $this->nullableString(
                    $data['promotion_reason']
                    ?? null
                ),

            'notes' =>
                $this->nullableString(
                    $data['notes']
                    ?? null
                ),

            'document_file_path' =>
                $document[
                    'document_file_path'
                ],

            'document_original_name' =>
                $document[
                    'document_original_name'
                ],

            'document_mime_type' =>
                $document[
                    'document_mime_type'
                ],

            'document_file_size' =>
                $document[
                    'document_file_size'
                ],

            'updated_at' =>
                date('Y-m-d H:i:s')
        ]
    );
}


    public function toggleStatus(
        int $id
    ): void {
        $promotion =
            $this->promotions->find($id);

        if ($promotion === null) {
            throw new RuntimeException(
                'Promotion record not found.'
            );
        }

        $currentStatus = (int) (
            $promotion['status']
            ?? 0
        );

        $newStatus =
            $currentStatus === 1
                ? 0
                : 1;

        $this->promotions->updateStatus(
            $id,
            $newStatus
        );
    }

    private function findRank(
        int $id
    ): ?array {
        return $this->query
            ->table('ranks')
            ->find($id);
    }

    private function uploadDocument(
        array $file,
        string $employeeCode
    ): array {
        $uploadError = (int) (
            $file['error']
            ?? UPLOAD_ERR_NO_FILE
        );

        if (
            $uploadError
            !== UPLOAD_ERR_OK
        ) {
            throw new RuntimeException(
                'Promotion document upload failed. Error code: '
                . $uploadError
            );
        }

        $fileSize = (int) (
            $file['size']
            ?? 0
        );

        if ($fileSize < 1) {
            throw new RuntimeException(
                'Uploaded promotion document is empty.'
            );
        }

        if (
            $fileSize >
            (10 * 1024 * 1024)
        ) {
            throw new RuntimeException(
                'Promotion document must not exceed 10 MB.'
            );
        }

        $temporaryFile = (string) (
            $file['tmp_name']
            ?? ''
        );

        if (
            $temporaryFile === ''
            || !is_uploaded_file(
                $temporaryFile
            )
        ) {
            throw new RuntimeException(
                'Invalid uploaded promotion document.'
            );
        }

        $mimeType = null;

        /*
        |--------------------------------------------------------------------------
        | METHOD 1: finfo
        |--------------------------------------------------------------------------
        */

        if (class_exists('\finfo')) {
            $fileInfo =
                new \finfo(
                    FILEINFO_MIME_TYPE
                );

            $detectedType =
                $fileInfo->file(
                    $temporaryFile
                );

            if (
                is_string($detectedType)
                && $detectedType !== ''
            ) {
                $mimeType =
                    $detectedType;
            }
        }

        /*
        |--------------------------------------------------------------------------
        | METHOD 2: mime_content_type
        |--------------------------------------------------------------------------
        */

        if (
            $mimeType === null
            && function_exists(
                'mime_content_type'
            )
        ) {
            $detectedType =
                \mime_content_type(
                    $temporaryFile
                );

            if (
                is_string($detectedType)
                && $detectedType !== ''
            ) {
                $mimeType =
                    $detectedType;
            }
        }

        /*
        |--------------------------------------------------------------------------
        | METHOD 3: Browser MIME fallback
        |--------------------------------------------------------------------------
        */

        if ($mimeType === null) {
            $browserMime = trim(
                (string) (
                    $file['type']
                    ?? ''
                )
            );

            if ($browserMime !== '') {
                $mimeType =
                    $browserMime;
            }
        }

        if ($mimeType === null) {
            throw new RuntimeException(
                'Unable to determine promotion document type.'
            );
        }

        $allowedTypes = [
            'application/pdf' =>
                'pdf',

            'image/jpeg' =>
                'jpg',

            'image/png' =>
                'png',

            'application/msword' =>
                'doc',

            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' =>
                'docx'
        ];

        if (
            !isset(
                $allowedTypes[$mimeType]
            )
        ) {
            throw new RuntimeException(
                'Only PDF, JPG, PNG, DOC and DOCX files are allowed.'
            );
        }

        $extension =
            $allowedTypes[$mimeType];

        $directory =
            PUBLIC_PATH
            . '/uploads/employees/'
            . $employeeCode
            . '/promotions';

        if (
            !is_dir($directory)
            && !mkdir(
                $directory,
                0755,
                true
            )
            && !is_dir($directory)
        ) {
            throw new RuntimeException(
                'Unable to create employee promotion directory.'
            );
        }

        if (!is_writable($directory)) {
            throw new RuntimeException(
                'Employee promotion directory is not writable.'
            );
        }

        $fileName =
            $employeeCode
            . '_PROMOTION_'
            . date('Ymd_His')
            . '_'
            . bin2hex(
                random_bytes(4)
            )
            . '.'
            . $extension;

        $destination =
            $directory
            . '/'
            . $fileName;

        if (
            !move_uploaded_file(
                $temporaryFile,
                $destination
            )
        ) {
            throw new RuntimeException(
                'Unable to save promotion document.'
            );
        }

        return [
            'document_file_path' =>
                'uploads/employees/'
                . $employeeCode
                . '/promotions/'
                . $fileName,

            'document_original_name' =>
                (string) (
                    $file['name']
                    ?? $fileName
                ),

            'document_mime_type' =>
                $mimeType,

            'document_file_size' =>
                $fileSize
        ];
    }

    private function nullableString(
        mixed $value
    ): ?string {
        $value = trim(
            (string) $value
        );

        return $value !== ''
            ? $value
            : null;
    }
}