<?php

namespace App\Controllers\Test;

use App\Controllers\BaseController;
use App\Libraries\TestValidationService;
use App\Libraries\ValueSet;
use App\Traits\PatchValidationTrait;
use App\Traits\ResponseTrait;

class TestsController extends BaseController
{
    use ResponseTrait;
    use PatchValidationTrait;

    protected $model;
    protected $modelCal;
    protected $modelGrp;
    protected $modelMap;
    protected $modelMapDetail;
    protected $modelRefNum;
    protected $modelRefTxt;
    protected $rules;

    public function __construct()
    {
        $this->model = new \App\Models\Test\TestDefSiteModel;
        $this->modelCal = new \App\Models\Test\TestDefCalModel;
        $this->modelGrp = new \App\Models\Test\TestDefGrpModel;
        $this->modelMap = new \App\Models\Test\TestMapModel;
        $this->modelMapDetail = new \App\Models\Test\TestMapDetailModel;
        $this->modelRefNum = new \App\Models\RefRange\RefNumModel;
        $this->modelRefTxt = new \App\Models\RefRange\RefTxtModel;

        $this->rules = [
            'TestSiteCode' => 'required',
            'TestSiteName' => 'required',
            'TestType'     => 'required',
        ];
    }

    public function index()
    {
        $search = $this->request->getGet('search');
        
        $filters = [
            'SiteID' => $this->request->getGet('SiteID'),
            'TestType' => $this->request->getGet('TestType'),
            'isVisibleScr' => $this->request->getGet('isVisibleScr'),
            'isVisibleRpt' => $this->request->getGet('isVisibleRpt'),
            'TestSiteName' => $this->request->getGet('TestSiteName'),
            'TestSiteCode' => $this->request->getGet('TestSiteCode'),
            'search' => $search,
        ];

        $rows = $this->model->getTestsWithRelations($filters);

        if (empty($rows)) {
            return $this->respond([
                'status'  => 'success',
                'message' => 'No data.',
                'data'    => [],
            ], 200);
        }

        $rows = ValueSet::transformLabels($rows, [
            'TestType' => 'test_type',
        ]);

        return $this->respond([
            'status'  => 'success',
            'message' => 'Data fetched successfully',
            'data'    => $rows,
        ], 200);
    }

    public function show($id = null)
    {
        if (!$id) {
            return $this->failValidationErrors('TestSiteID is required');
        }

        $row = $this->model->getTestById($id);

        if (!$row) {
            return $this->respond([
                'status'  => 'success',
                'message' => 'No data.',
                'data'    => null,
            ], 200);
        }

        $typeCode = $row['TestType'] ?? '';

        if ($typeCode === 'CALC') {
            $row['testdefcal'] = $this->modelCal->getByTestSiteID($id);
            $row['testdefgrp'] = [
                'members' => $this->modelGrp->getGroupMembers($id),
            ];
        } elseif ($typeCode === 'GROUP') {
            $row['testdefgrp'] = [
                'members' => $this->modelGrp->getGroupMembers($id),
            ];
        } elseif ($typeCode !== 'TITLE') {
            $refType = $row['RefType'] ?? '';
            $resultType = $row['ResultType'] ?? '';

            if (TestValidationService::usesRefNum($resultType, $refType)) {
                $row['refnum'] = $this->modelRefNum->getFormattedByTestSiteID($id);
            }

            if (TestValidationService::usesRefTxt($resultType, $refType)) {
                $row['reftxt'] = $this->modelRefTxt->getFormattedByTestSiteID($id);
            }
        }

        // Keep /api/test payload focused on test definition fields.
        unset($row['testmap']);

        return $this->respond([
            'status'  => 'success',
            'message' => 'Data fetched successfully',
            'data'    => $row,
        ], 200);
    }

    public function create()
    {
        $input = $this->request->getJSON(true);

        if (!$this->validateData($input, $this->rules)) {
            return $this->failValidationErrors($this->validator->getErrors());
        }

        $testType = $input['TestType'] ?? '';
        $details = $input['details'] ?? $input;
        $resultType = $details['ResultType'] ?? '';
        $refType = $details['RefType'] ?? '';

        if (TestValidationService::isCalc($testType)) {
            $resultType = 'NMRIC';
            $refType = $refType ?: 'RANGE';
        } elseif (TestValidationService::isGroup($testType) || TestValidationService::isTitle($testType)) {
            $resultType = 'NORES';
            $refType = 'NOREF';
        }

        if ($resultType && $refType) {
            $validation = TestValidationService::validate($testType, $resultType, $refType);
            if (!$validation['valid']) {
                return $this->failValidationErrors(['type_validation' => $validation['error']]);
            }
        }

        $db = \Config\Database::connect();
        $db->transStart();

        try {
            $testSiteData = [
                'SiteID'      => array_key_exists('SiteID', $input) ? $input['SiteID'] : null,
                'TestSiteCode'=> $input['TestSiteCode'],
                'TestSiteName'=> $input['TestSiteName'],
                'TestType'    => $input['TestType'],
                'Description' => $input['Description'] ?? null,
                'SeqScr'      => array_key_exists('SeqScr', $input) ? $input['SeqScr'] : null,
                'SeqRpt'      => array_key_exists('SeqRpt', $input) ? $input['SeqRpt'] : null,
                'IndentLeft'  => $input['IndentLeft'] ?? 0,
                'FontStyle'   => $input['FontStyle'] ?? null,
                'isVisibleScr'  => $input['isVisibleScr'] ?? 1,
                'isVisibleRpt'  => $input['isVisibleRpt'] ?? 1,
                'isCountStat'   => $input['isCountStat'] ?? 1,
                'isRequestable' => $input['isRequestable'] ?? 1,
                'StartDate'   => $input['StartDate'] ?? date('Y-m-d H:i:s'),
            ];

            $id = $this->model->insert($testSiteData);
            if (!$id) {
                $dbError = $db->error();
                log_message('error', 'Test insert failed: ' . json_encode($dbError, JSON_UNESCAPED_SLASHES));
                $message = $dbError['message'] ?? 'Failed to insert main test definition';
                throw new \Exception('Failed to insert main test definition: ' . $message);
            }

            $this->handleDetails($id, $input, 'insert');

            $db->transComplete();

            if ($db->transStatus() === false) {
                $dbError = $db->error();
                $lastQuery = $db->showLastQuery();
                log_message('error', 'TestController transaction failed: ' . json_encode([
                    'error' => $dbError,
                    'last_query' => $lastQuery,
                ], JSON_UNESCAPED_SLASHES));
                return $this->failServerError('Transaction failed');
            }

            return $this->respondCreated([
                'status'  => 'success',
                'message' => 'Test created successfully',
                'data'    => ['TestSiteId' => $id],
            ]);
        } catch (\Exception $e) {
            $db->transRollback();

            return $this->failServerError('Something went wrong: ' . $e->getMessage());
        }
    }

    public function update($id = null)
    {
        $input = $this->requirePatchPayload($this->request->getJSON(true));
        if ($input === null) {
            return;
        }

        if (!$id && isset($input['TestSiteID'])) {
            $id = $input['TestSiteID'];
        }

        $id = $this->requirePatchId($id, 'TestSiteID');
        if ($id === null) {
            return;
        }

        $existing = $this->model->find($id);
        if (!$existing) {
            return $this->respond([
                'status' => 'failed',
                'message' => 'Test not found',
                'data' => []
            ], 404);
        }

        $testType = $input['TestType'] ?? $existing['TestType'] ?? '';
        $details = $input['details'] ?? $input;
        $resultType = $details['ResultType'] ?? $existing['ResultType'] ?? '';
        $refType = $details['RefType'] ?? $existing['RefType'] ?? '';

        if (TestValidationService::isCalc($testType)) {
            $resultType = 'NMRIC';
            $refType = $refType ?: 'RANGE';
        } elseif (TestValidationService::isGroup($testType) || TestValidationService::isTitle($testType)) {
            $resultType = 'NORES';
            $refType = 'NOREF';
        }

        if ($resultType && $refType) {
            $validation = TestValidationService::validate($testType, $resultType, $refType);
            if (!$validation['valid']) {
                return $this->failValidationErrors(['type_validation' => $validation['error']]);
            }
        }

        $db = \Config\Database::connect();
        $db->transStart();

        try {
            $testSiteData = [];
            $allowedUpdateFields = [
                'TestSiteCode',
                'TestSiteName',
                'TestType',
                'Description',
                'SeqScr',
                'SeqRpt',
                'IndentLeft',
                'FontStyle',
            'isVisibleScr',
            'isVisibleRpt',
            'isCountStat',
            'isRequestable',
            'StartDate',
        ];

            foreach ($allowedUpdateFields as $field) {
                if (array_key_exists($field, $input)) {
                    $testSiteData[$field] = $input[$field];
                }
            }

            if (!empty($testSiteData)) {
                $this->model->update($id, $testSiteData);
            }

            $this->handleDetails($id, $input, 'update');

            $db->transComplete();

            if ($db->transStatus() === false) {
                return $this->failServerError('Transaction failed');
            }

            return $this->respond([
                'status'  => 'success',
                'message' => 'Test updated successfully',
                'data'    => ['TestSiteId' => $id],
            ]);
        } catch (\Exception $e) {
            $db->transRollback();

            return $this->failServerError('Something went wrong: ' . $e->getMessage());
        }
    }

    public function delete($id = null)
    {
        $input = $this->request->getJSON(true);

        if (!$id && isset($input['TestSiteID'])) {
            $id = $input['TestSiteID'];
        }
        if (!$id) {
            return $this->failValidationErrors('TestSiteID is required.');
        }

        $existing = $this->model->find($id);
        if (!$existing) {
            return $this->failNotFound('Test not found');
        }

        if (!empty($existing['EndDate'])) {
            return $this->failValidationErrors('Test is already disabled');
        }

        $db = \Config\Database::connect();
        $db->transStart();

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

            $this->model->update($id, ['EndDate' => $now]);

            $testType = $existing['TestType'];
            $typeCode = $testType;

            if (TestValidationService::isCalc($typeCode)) {
                $this->modelCal->disableByTestSiteID($id);
                $this->modelGrp->disableByTestSiteID($id);
            } elseif (TestValidationService::isGroup($typeCode)) {
                $this->modelGrp->disableByTestSiteID($id);
            } elseif (TestValidationService::isTechnicalTest($typeCode)) {
                $this->modelRefNum->disableByTestSiteID($id);
                $this->modelRefTxt->disableByTestSiteID($id);
            }

            // Disable testmap by test code
            $testSiteCode = $existing['TestSiteCode'] ?? null;
            if ($testSiteCode) {
                $existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode);
                foreach ($existingMaps as $existingMap) {
                    $this->modelMapDetail->disableByTestMapID($existingMap['TestMapID']);
                    $this->modelMap->update($existingMap['TestMapID'], ['EndDate' => $now]);
                }
            }

            $db->transComplete();

            if ($db->transStatus() === false) {
                return $this->failServerError('Transaction failed');
            }

            return $this->respond([
                'status'  => 'success',
                'message' => 'Test disabled successfully',
                'data'    => ['TestSiteId' => $id, 'EndDate' => $now],
            ]);
        } catch (\Exception $e) {
            $db->transRollback();

            return $this->failServerError('Something went wrong: ' . $e->getMessage());
        }
    }

    private function handleDetails($testSiteID, $input, $action)
    {
        $testTypeID = $input['TestType'] ?? null;
        $testSiteCode = null;

        if (!$testTypeID && $action === 'update') {
            $existing = $this->model->find($testSiteID);
            $testTypeID = $existing['TestType'] ?? null;
            $testSiteCode = $existing['TestSiteCode'] ?? null;
        }

        if (!$testTypeID) {
            return;
        }

        $typeCode = $testTypeID;

        $details = $input['details'] ?? $input;
        $details['TestSiteID'] = $testSiteID;
        $details['SiteID'] = array_key_exists('SiteID', $input) ? $input['SiteID'] : null;

        switch ($typeCode) {
            case 'CALC':
                $this->saveCalcDetails($testSiteID, $details, $input, $action);

                break;

            case 'GROUP':
                $this->saveGroupDetails($testSiteID, $details, $input, $action);

                break;

            case 'TITLE':
                break;

            case 'TEST':
            case 'PARAM':
            default:
                $this->saveTechDetails($testSiteID, $details, $action, $typeCode);

                if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) {
                    $refType = (string) $details['RefType'];
                    $resultType = $details['ResultType'] ?? '';

                    if (TestValidationService::usesRefNum($resultType, $refType) && isset($input['refnum']) && is_array($input['refnum'])) {
                        $this->saveRefNumRanges($testSiteID, $input['refnum'], $action, array_key_exists('SiteID', $input) ? $input['SiteID'] : null);
                    }

                    if (TestValidationService::usesRefTxt($resultType, $refType) && isset($input['reftxt']) && is_array($input['reftxt'])) {
                        $this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, array_key_exists('SiteID', $input) ? $input['SiteID'] : null);
                    }
                }

                break;
        }

    }

    private function saveTechDetails($testSiteID, $data, $action, $typeCode)
    {
        $allowedFields = [
            'DisciplineID',
            'DepartmentID',
            'ResultType',
            'RefType',
            'VSet',
            'ReqQty',
            'ReqQtyUnit',
            'Unit1',
            'Factor',
            'Unit2',
            'Decimal',
            'CollReq',
            'Method',
            'ExpectedTAT',
        ];

        $techData = [];
        foreach ($allowedFields as $field) {
            if (array_key_exists($field, $data)) {
                $techData[$field] = $data[$field];
            }
        }

        if ($techData !== []) {
            $this->model->update($testSiteID, $techData);
        }
    }

    private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID)
    {
        if ($action === 'update') {
            $this->modelRefNum->disableByTestSiteID($testSiteID);
        }

        $this->modelRefNum->batchInsert($testSiteID, $siteID, $ranges);
    }

    private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID)
    {
        if ($action === 'update') {
            $this->modelRefTxt->disableByTestSiteID($testSiteID);
        }

        $this->modelRefTxt->batchInsert($testSiteID, $siteID, $ranges);
    }

    private function saveCalcDetails($testSiteID, $data, $input, $action)
    {
        $calcData = [];
        $fieldMap = [
            'DisciplineID' => 'DisciplineID',
            'DepartmentID' => 'DepartmentID',
            'Factor' => 'Factor',
            'Unit2' => 'Unit2',
            'Decimal' => 'Decimal',
            'Method' => 'Method',
        ];

        foreach ($fieldMap as $source => $target) {
            if (array_key_exists($source, $data)) {
                $calcData[$target] = $data[$source];
            }
        }

        if (array_key_exists('FormulaCode', $data) || array_key_exists('Formula', $data)) {
            $calcData['FormulaCode'] = $data['FormulaCode'] ?? $data['Formula'] ?? null;
        }

        if (array_key_exists('RefType', $data)) {
            $calcData['RefType'] = $data['RefType'];
        }

        if (array_key_exists('Unit1', $data) || array_key_exists('ResultUnit', $data)) {
            $calcData['Unit1'] = $data['Unit1'] ?? $data['ResultUnit'] ?? null;
        }

        $hasMemberPayload = isset($input['testdefgrp'])
            && is_array($input['testdefgrp'])
            && array_key_exists('members', $input['testdefgrp']);

        if ($action === 'insert' && !array_key_exists('ResultType', $calcData)) {
            $calcData['ResultType'] = 'NMRIC';
        }

        if ($action === 'insert' && !array_key_exists('RefType', $calcData)) {
            $calcData['RefType'] = 'RANGE';
        }

        if ($calcData !== []) {
            $calcData['TestSiteID'] = $testSiteID;
            if ($action === 'update') {
                $exists = $this->modelCal->existsByTestSiteID($testSiteID);

                if ($exists) {
                    unset($calcData['TestSiteID']);
                    $this->modelCal->update($exists['TestCalID'], $calcData);
                } else {
                    if (!array_key_exists('ResultType', $calcData)) {
                        $calcData['ResultType'] = 'NMRIC';
                    }
                    if (!array_key_exists('RefType', $calcData)) {
                        $calcData['RefType'] = 'RANGE';
                    }
                    $this->modelCal->insert($calcData);
                }
            } else {
                $this->modelCal->insert($calcData);
            }
        }

        if ($action === 'update' && !$hasMemberPayload) {
            return;
        }

        if ($action === 'update') {
            $this->modelGrp->disableByTestSiteID($testSiteID);
        }

        $memberIDs = $this->resolveMemberIDs($input);

        $validation = $this->validateMemberIDs($memberIDs);
        if (!$validation['valid']) {
            throw new \Exception('Invalid member TestSiteID(s): ' . implode(', ', $validation['invalid']) . '. Make sure to use TestSiteID, not SeqScr or other values.');
        }

        foreach ($memberIDs as $memberID) {
            $this->modelGrp->insert([
                'TestSiteID' => $testSiteID,
                'Member'     => $memberID,
            ]);
        }
    }

    private function resolveMemberIDs(array $input): array
    {
        $memberIDs = [];

        $rawMembers = $input['testdefgrp']['members'] ?? [];
        if (is_array($rawMembers)) {
            foreach ($rawMembers as $member) {
                if (is_array($member)) {
                    $rawID = $member['TestSiteID'] ?? null;
                } else {
                    $rawID = is_numeric($member) ? $member : null;
                }

                if ($rawID !== null && is_numeric($rawID)) {
                    $memberIDs[] = (int) $rawID;
                }
            }
        }

        $memberIDs = array_values(array_unique(array_filter($memberIDs)));

        return $memberIDs;
    }

    /**
     * Validate that member IDs exist in testdefsite table
     *
     * @param array $memberIDs Array of TestSiteID values to validate
     * @return array ['valid' => bool, 'invalid' => array]
     */
    private function validateMemberIDs(array $memberIDs): array
    {
        if (empty($memberIDs)) {
            return ['valid' => true, 'invalid' => []];
        }

        $existing = $this->model->whereIn('TestSiteID', $memberIDs)
                                ->where('EndDate IS NULL')
                                ->findAll();

        $existingIDs = array_column($existing, 'TestSiteID');
        $invalidIDs = array_diff($memberIDs, $existingIDs);

        return [
            'valid' => empty($invalidIDs),
            'invalid' => array_values($invalidIDs)
        ];
    }

    private function saveGroupDetails($testSiteID, $data, $input, $action)
    {
        $hasMemberPayload = isset($input['testdefgrp'])
            && is_array($input['testdefgrp'])
            && array_key_exists('members', $input['testdefgrp']);

        if ($action === 'update' && !$hasMemberPayload) {
            return;
        }

        if ($action === 'update') {
            $this->modelGrp->disableByTestSiteID($testSiteID);
        }

        $memberIDs = $this->resolveMemberIDs($input);

        // Validate member IDs before insertion
        $validation = $this->validateMemberIDs($memberIDs);
        if (!$validation['valid']) {
            throw new \Exception('Invalid member TestSiteID(s): ' . implode(', ', $validation['invalid']) . '. Make sure to use TestSiteID, not SeqScr or other values.');
        }

        foreach ($memberIDs as $memberID) {
            $this->modelGrp->insert([
                'TestSiteID' => $testSiteID,
                'Member'     => $memberID,
            ]);
        }
    }

    private function saveTestMap($testSiteID, $testSiteCode, $mappings, $action)
    {
        if ($action === 'update' && $testSiteCode) {
            // Find existing mappings by test code through testmapdetail
            $existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode);

            foreach ($existingMaps as $existingMap) {
                $this->modelMapDetail->disableByTestMapID($existingMap['TestMapID']);
            }

            // Soft delete the testmap headers
            foreach ($existingMaps as $existingMap) {
                $this->modelMap->update($existingMap['TestMapID'], ['EndDate' => date('Y-m-d H:i:s')]);
            }
        }

        foreach ($this->normalizeTestMapPayload($mappings) as $map) {
            $mapData = [
                'HostType'   => $map['HostType'] ?? null,
                'HostID'     => $map['HostID'] ?? null,
                'ClientType' => $map['ClientType'] ?? null,
                'ClientID'   => $map['ClientID'] ?? null,
            ];

            $testMapID = $this->modelMap->insert($mapData);
            if (!$testMapID) {
                continue;
            }

            foreach ($this->extractTestMapDetails($map) as $detail) {
                $detailData = [
                    'TestMapID'      => $testMapID,
                    'HostTestCode'   => $detail['HostTestCode'] ?? null,
                    'HostTestName'   => $detail['HostTestName'] ?? null,
                    'ConDefID'       => $detail['ConDefID'] ?? null,
                    'ClientTestCode' => $detail['ClientTestCode'] ?? null,
                    'ClientTestName' => $detail['ClientTestName'] ?? null,
                ];
                $this->modelMapDetail->insert($detailData);
            }
        }
    }

    private function normalizeTestMapPayload($mappings): array
    {
        if (!is_array($mappings)) {
            return [];
        }

        if ($this->isAssoc($mappings)) {
            return [$mappings];
        }

        return array_values(array_filter($mappings, static fn ($map) => is_array($map)));
    }

    private function extractTestMapDetails(array $map): array
    {
        if (isset($map['details']) && is_array($map['details'])) {
            return array_values(array_filter($map['details'], static fn ($detail) => is_array($detail)));
        }

        $flatDetail = [
            'HostTestCode'   => $map['HostTestCode'] ?? null,
            'HostTestName'   => $map['HostTestName'] ?? null,
            'ConDefID'       => $map['ConDefID'] ?? null,
            'ClientTestCode' => $map['ClientTestCode'] ?? null,
            'ClientTestName' => $map['ClientTestName'] ?? null,
        ];

        foreach ($flatDetail as $value) {
            if ($value !== null && $value !== '') {
                return [$flatDetail];
            }
        }

        return [];
    }

    private function isAssoc(array $array): bool
    {
        if ($array === []) {
            return false;
        }

        return array_keys($array) !== range(0, count($array) - 1);
    }
}
