<?php
namespace App\Controllers\Test;

use App\Traits\PatchValidationTrait;
use App\Traits\ResponseTrait;
use App\Controllers\BaseController;
use App\Libraries\ValueSet;
use App\Models\Test\TestMapModel;
use App\Models\Test\TestMapDetailModel;

class TestMapController extends BaseController {
    use ResponseTrait;
    use PatchValidationTrait;

    protected $db;
    protected $rules;
    protected $patchRules;
    protected $model;
    protected $modelDetail;
    protected array $headerFields = ['HostType', 'HostID', 'ClientType', 'ClientID'];
    protected array $detailFields = ['HostTestCode', 'HostTestName', 'ConDefID', 'ClientTestCode', 'ClientTestName'];
    protected array $detailRules;
    protected array $detailPatchRules;

    public function __construct() {
        $this->db = \Config\Database::connect();
        $this->model = new TestMapModel;
        $this->modelDetail = new TestMapDetailModel;
        $this->rules = [
            'HostID' => 'required|integer',
            'ClientID' => 'required|integer',
        ];
        $this->patchRules = [
            'HostID' => 'permit_empty|integer',
            'ClientID' => 'permit_empty|integer',
            'HostType' => 'permit_empty|string',
            'ClientType' => 'permit_empty|string',
        ];
        $this->detailRules = [
            'HostTestCode' => 'permit_empty|max_length[10]',
            'HostTestName' => 'permit_empty|max_length[100]',
            'ConDefID' => 'permit_empty|integer',
            'ClientTestCode' => 'permit_empty|max_length[10]',
            'ClientTestName' => 'permit_empty|max_length[100]',
        ];
        $this->detailPatchRules = $this->detailRules;
    }

    public function index() {
        $rows = $this->model->getUniqueGroupings();
        if (empty($rows)) { return $this->respond([ 'status'  => 'success', 'message' => "no Data.", 'data' => [] ], 200); }

        $rows = ValueSet::transformLabels($rows, [
            'HostType' => 'entity_type',
            'ClientType' => 'entity_type',
        ]);

        $rows = array_map([$this, 'sanitizeTopLevelPayload'], $rows);

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

    public function show($id = null) {
        $row = $this->model->where('TestMapID',$id)->where('EndDate', null)->first();
        if (empty($row)) { return $this->respond([ 'status'  => 'success', 'message' => "no Data.", 'data' => null ], 200); }

        $row = ValueSet::transformLabels([$row], [
            'HostType' => 'entity_type',
            'ClientType' => 'entity_type',
        ])[0];

        $row = $this->sanitizeTopLevelPayload($row);

        // Include testmapdetail records
        $row['details'] = $this->modelDetail->getDetailsByTestMap($id);

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

    public function create() {
        $input = $this->request->getJSON(true);
        $detailsPayload = null;
        if (array_key_exists('details', $input)) {
            $detailsPayload = $this->resolveDetailOperations($input['details']);
            if ($detailsPayload === null) { return; }
        }

        $headerInput = array_intersect_key($input, array_flip($this->headerFields));
        if (!$this->validateData($headerInput, $this->rules)) {
            log_message('error', 'TestMap create validation failed: ' . json_encode($this->validator->getErrors()));
            return $this->failValidationErrors($this->validator->getErrors());
        }

        $this->db->transStart();
        try {
            $id = $this->model->insert($headerInput);

            if ($detailsPayload !== null && !empty($detailsPayload['created'])) {
                if (!$this->insertDetailRows($id, $detailsPayload['created'])) {
                    $this->db->transRollback();
                    return;
                }
            }

            $this->db->transComplete();
            if ($this->db->transStatus() === false) {
                return $this->failServerError('Something went wrong while saving the test map.');
            }

            return $this->respondCreated([ 'status'  => 'success', 'message' => "data created successfully", 'data' => $id ]);
        } catch (\Exception $e) {
            $this->db->transRollback();
            return $this->failServerError('Something went wrong: ' . $e->getMessage());
        }
    }

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

        $detailsPayload = null;
        if (array_key_exists('details', $input)) {
            $detailsPayload = $this->resolveDetailOperations($input['details']);
            if ($detailsPayload === null) {
                return;
            }
        }

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

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

        if (isset($input['TestMapID']) && (string) $input['TestMapID'] !== (string) $id) {
            return $this->failValidationErrors('TestMapID in URL does not match body.');
        }

        $validationInput = array_intersect_key($headerInput = array_intersect_key($input, array_flip($this->headerFields)), $this->patchRules);
        if (!empty($validationInput) && !$this->validateData($validationInput, $this->patchRules)) {
            return $this->failValidationErrors($this->validator->getErrors());
        }

        $input['TestMapID'] = $id;
        $this->db->transStart();
        try {
            if (!empty($headerInput)) {
                $this->model->update($id, $headerInput);
            }

            if ($detailsPayload !== null && !$this->applyDetailOperations($id, $detailsPayload)) {
                $this->db->transRollback();
                return;
            }

            $this->db->transComplete();
            if ($this->db->transStatus() === false) {
                return $this->failServerError('Something went wrong while updating the test map.');
            }

            return $this->respond([ 'status'  => 'success', 'message' => 'data updated successfully', 'data' => $id ], 200);
        } catch (\Exception $e) {
            $this->db->transRollback();
            return $this->failServerError('Something went wrong: ' . $e->getMessage());
        }
    }

    public function delete() {
        $input = $this->request->getJSON(true);
        $id = $input["TestMapID"] ?? null;
        if (!$id) { return $this->failValidationErrors('TestMapID is required.'); }
        
        try {
                $row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
            if (empty($row)) { return $this->respond([ 'status'  => 'failed', 'message' => "Data not found or already deleted.", 'data' => null ], 404); }
            
            $this->db->transStart();
            $timestamp = date('Y-m-d H:i:s');
            $this->model->update($id, ['EndDate' => $timestamp]);

            $this->modelDetail->where('TestMapID', $id)
                ->where('EndDate', null)
                ->set('EndDate', $timestamp)
                ->update();

            $this->db->transComplete();

            if ($this->db->transStatus() === false) {
                return $this->failServerError('Something went wrong while deleting the test map.');
            }

            return $this->respond([ 'status'  => 'success', 'message' => "data deleted successfully", 'data' => $id ], 200);
        } catch (\Exception $e) {
            return $this->failServerError('Something went wrong: ' . $e->getMessage());
        }
    }

    public function showByTestCode($testCode = null) {
        if (!$testCode) { return $this->failValidationErrors('TestCode is required.'); }
        
        $rows = $this->model->getMappingsByTestCode($testCode);
        if (empty($rows)) { return $this->respond([ 'status'  => 'success', 'message' => "no Data.", 'data' => [] ], 200); }

        $rows = ValueSet::transformLabels($rows, [
            'HostType' => 'entity_type',
            'ClientType' => 'entity_type',
        ]);

        $rows = array_map([$this, 'sanitizeTopLevelPayload'], $rows);

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

    private function sanitizeTopLevelPayload(array $row): array
    {
        unset($row['TestCode'], $row['testcode']);
        return $row;
    }

    private function resolveDetailOperations(mixed $detailsPayload): ?array
    {
        if ($detailsPayload === null) {
            return null;
        }

        if (!is_array($detailsPayload)) {
            $this->failValidationErrors('details must be an array or object.');
            return null;
        }

        if ($this->isDetailOpsPayload($detailsPayload)) {
            $createdItems = $this->normalizeDetailList($detailsPayload['created'] ?? [], 'details.created');
            if ($createdItems === null) { return null; }
            $editedItems = $this->normalizeDetailList($detailsPayload['edited'] ?? [], 'details.edited');
            if ($editedItems === null) { return null; }
            $deletedIds = $this->normalizeDetailIds($detailsPayload['deleted'] ?? []);
            if ($deletedIds === null) { return null; }

            return ['created' => $createdItems, 'edited' => $editedItems, 'deleted' => $deletedIds];
        }

        if ($this->isListPayload($detailsPayload)) {
            $items = $this->normalizeDetailList($detailsPayload, 'details');
            if ($items === null) { return null; }
            return ['created' => $items, 'edited' => [], 'deleted' => []];
        }

        if ($this->isAssocArray($detailsPayload)) {
            $items = $this->normalizeDetailList([$detailsPayload], 'details');
            if ($items === null) { return null; }
            return ['created' => $items, 'edited' => [], 'deleted' => []];
        }

        $this->failValidationErrors('details must be an array of objects or contain created/edited/deleted arrays.');
        return null;
    }

    private function applyDetailOperations(int $testMapID, array $operations): bool
    {
        if (!empty($operations['edited']) && !$this->updateDetails($testMapID, $operations['edited'])) {
            return false;
        }

        if (!empty($operations['deleted']) && !$this->softDeleteDetails($testMapID, $operations['deleted'])) {
            return false;
        }

        if (!empty($operations['created']) && !$this->insertDetailRows($testMapID, $operations['created'])) {
            return false;
        }

        return true;
    }

    private function insertDetailRows(int $testMapID, array $items): bool
    {
        if (empty($items)) {
            return true;
        }

        $prepared = [];
        foreach ($items as $index => $item) {
            if (!$this->validateData($item, $this->detailRules)) {
                $this->failValidationErrors(['details.created' => $this->validator->getErrors()]);
                return false;
            }
            $prepared[] = array_merge(['TestMapID' => $testMapID], $item);
        }

        $this->modelDetail->insertBatch($prepared);
        return true;
    }

    private function updateDetails(int $testMapID, array $items): bool
    {
        foreach ($items as $index => $detail) {
            $detailID = $detail['TestMapDetailID'] ?? null;
            if (!$detailID || !ctype_digit((string) $detailID)) {
                $this->failValidationErrors("details.edited[{$index}].TestMapDetailID is required and must be an integer.");
                return false;
            }

            if (array_key_exists('TestMapID', $detail) && (int) $detail['TestMapID'] !== $testMapID) {
                $this->failValidationErrors("details.edited[{$index}] must belong to TestMap {$testMapID}.");
                return false;
            }

            $existing = $this->modelDetail->where('TestMapDetailID', $detailID)
                ->where('TestMapID', $testMapID)
                ->where('EndDate', null)
                ->first();

            if (empty($existing)) {
                $this->failValidationErrors("Detail record {$detailID} not found for this test map.");
                return false;
            }

            $updateData = array_intersect_key($detail, array_flip($this->detailFields));
            if ($updateData === []) {
                continue;
            }

            if (!$this->validateData($updateData, $this->detailPatchRules)) {
                $this->failValidationErrors($this->validator->getErrors());
                return false;
            }

            $this->modelDetail->update($detailID, $updateData);
        }

        return true;
    }

    private function softDeleteDetails(int $testMapID, array $ids): bool
    {
        if (empty($ids)) {
            return true;
        }

        $existing = $this->modelDetail->select('TestMapDetailID')
            ->whereIn('TestMapDetailID', $ids)
            ->where('TestMapID', $testMapID)
            ->where('EndDate', null)
            ->findAll();

        $foundIds = array_column($existing, 'TestMapDetailID');
        $missing = array_diff($ids, $foundIds);
        if (!empty($missing)) {
            $this->failValidationErrors('Some detail IDs do not exist or belong to another test map: ' . implode(', ', $missing));
            return false;
        }

        $this->modelDetail->whereIn('TestMapDetailID', $ids)
            ->where('TestMapID', $testMapID)
            ->where('EndDate', null)
            ->set('EndDate', date('Y-m-d H:i:s'))
            ->update();

        return true;
    }

    private function isDetailOpsPayload(array $payload): bool
    {
        return (bool) array_intersect(array_keys($payload), ['created', 'edited', 'deleted']);
    }

    private function isListPayload(array $payload): bool
    {
        if ($payload === []) {
            return true;
        }
        return array_keys($payload) === range(0, count($payload) - 1);
    }

    private function isAssocArray(array $payload): bool
    {
        if ($payload === []) {
            return false;
        }
        return array_keys($payload) !== range(0, count($payload) - 1);
    }

    private function normalizeDetailList(mixed $value, string $fieldPath): ?array
    {
        if ($value === null) {
            return [];
        }

        if (!is_array($value)) {
            $this->failValidationErrors("{$fieldPath} must be an array of objects.");
            return null;
        }

        if ($value !== [] && $this->isAssocArray($value)) {
            $value = [$value];
        }

        $results = [];
        foreach ($value as $index => $item) {
            if (!is_array($item)) {
                $this->failValidationErrors("{$fieldPath}[{$index}] must be an object.");
                return null;
            }
            $results[] = $item;
        }

        return $results;
    }

    private function normalizeDetailIds(mixed $value): ?array
    {
        if ($value === null) {
            return [];
        }

        if (!is_array($value)) {
            $value = [$value];
        }

        $results = [];
        foreach ($value as $index => $item) {
            if (!ctype_digit((string) $item)) {
                $this->failValidationErrors("details.deleted[{$index}] must be an integer.");
                return null;
            }
            $results[] = (int) $item;
        }

        return array_values(array_unique($results));
    }

    public function batchCreate() {
        $items = $this->request->getJSON(true);
        if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); }
        
        $results = ['success' => [], 'failed' => []];
        $this->db->transStart();
        
        foreach ($items as $index => $item) {
            if (!$this->validateData($item, $this->rules)) {
                $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()];
                continue;
            }
            try {
                $id = $this->model->insert($item);
                $results['success'][] = ['index' => $index, 'TestMapID' => $id];
            } catch (\Exception $e) {
                $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()];
            }
        }
        
        $this->db->transComplete();
        return $this->respond([
            'status' => empty($results['failed']) ? 'success' : 'partial',
            'message' => 'Batch create completed',
            'data' => $results
        ], 200);
    }

    public function batchUpdate() {
        $items = $this->request->getJSON(true);
        if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); }
        
        $results = ['success' => [], 'failed' => []];
        $this->db->transStart();
        
        foreach ($items as $index => $item) {
            $id = $item['TestMapID'] ?? null;
            if (!$id) {
                $results['failed'][] = ['index' => $index, 'error' => 'TestMapID required'];
                continue;
            }
            if (!$this->validateData($item, $this->rules)) {
                $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()];
                continue;
            }
            try {
                $this->model->update($id, $item);
                $results['success'][] = ['index' => $index, 'TestMapID' => $id];
            } catch (\Exception $e) {
                $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()];
            }
        }
        
        $this->db->transComplete();
        return $this->respond([
            'status' => empty($results['failed']) ? 'success' : 'partial',
            'message' => 'Batch update completed',
            'data' => $results
        ], 200);
    }

    public function batchDelete() {
        $ids = $this->request->getJSON(true);
        if (!is_array($ids)) { return $this->failValidationErrors('Expected array of TestMapIDs'); }
        
        $results = ['success' => [], 'failed' => []];
        $this->db->transStart();
        
        foreach ($ids as $id) {
            try {
            $row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
                if (empty($row)) {
                    $results['failed'][] = ['TestMapID' => $id, 'error' => 'Not found or already deleted'];
                    continue;
                }
                $this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]);
                $results['success'][] = $id;
            } catch (\Exception $e) {
                $results['failed'][] = ['TestMapID' => $id, 'error' => $e->getMessage()];
            }
        }
        
        $this->db->transComplete();
        return $this->respond([
            'status' => empty($results['failed']) ? 'success' : 'partial',
            'message' => 'Batch delete completed',
            'data' => $results
        ], 200);
    }

}
