<?php
namespace App\Models\Patient;

use App\Models\BaseModel;
use App\Libraries\ValueSet;

use App\Models\Patient\PatAttModel;
use App\Models\Patient\PatComModel;
use App\Models\Patient\PatIdtModel;

use App\Services\AuditService;

class PatientModel extends BaseModel {
  protected $table      = 'patient';
  protected $primaryKey = 'InternalPID';
  protected $allowedFields = ['PatientID', 'AlternatePID', 'Prefix', 'NameFirst', 'NameMiddle', 'NameMaiden', 'NameLast', 'Suffix', 'NameAlias', 'Sex', 'Birthdate', 'PlaceOfBirth', 'Street_1', 'Street_2', 'Street_3', 
    'City', 'Province', 'ZIP', 'EmailAddress1', 'EmailAddress2', 'Phone', 'MobilePhone', 'Custodian', 'AccountNumber', 'Country', 'Race', 'MaritalStatus', 'Religion', 'Ethnic', 'Citizenship', 
    'isDead', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ];
    
  protected $useTimestamps = true;
  protected $createdField = 'CreateDate';
  protected $updatedField = '';
  protected $useSoftDeletes = true;
  protected $deletedField  = 'DelDate';

  public function getPatients($filters = []) {
    $qname = "CONCAT_WS(' ', IFNULL(NameFirst,''), IFNULL(NameMiddle,''), IFNULL(NameLast,''))";

    $this->select("InternalPID, PatientID, $qname as FullName, Sex, Birthdate, EmailAddress1 as Email, MobilePhone");

    if (!empty($filters['Name'])) {
      $this->like($qname, $filters['Name'], 'both');
    }

    if (!empty($filters['InternalPID'])) {
      $this->where('InternalPID', $filters['InternalPID']);
    }

    if (!empty($filters['PatientID'])) {
      $this->like('PatientID', $filters['PatientID'], 'both');
    }

    if (!empty($filters['Birthdate'])) {
      $this->where('Birthdate', $filters['Birthdate']);
    }

    $rows = $this->findAll();
    $rows = ValueSet::transformLabels($rows, [
      'Sex' => 'sex',
    ]);

    foreach ($rows as &$row) {
      $birthdate = $row['Birthdate'] ?? null;
      $row['Age'] = empty($birthdate)
        ? ''
        : $this->calculateAgeFromBirthdate($birthdate, null);
      $row['BirthdateConversion'] = $this->formatedDateForDisplay($birthdate);
    }
    unset($row);

    return $rows;
  }

  public function getPatient($InternalPID) {
    $rows = $this->select("
          patient.*,
          patcom.Comment as Comment,
          patidt.IdentifierType,
          patidt.Identifier,
          patatt.Address,
          patient.Province as Province,
          areageo1.AreaName as ProvinceLabel,
          patient.City as City,
          areageo2.AreaName as CityLabel

      ")
      ->join('patcom', 'patcom.InternalPID = patient.InternalPID', 'left')
      ->join('patidt', 'patidt.InternalPID = patient.InternalPID', 'left')
      ->join('patatt', 'patatt.InternalPID = patient.InternalPID and patatt.DelDate is null', 'left')
      ->join('areageo areageo1', 'areageo1.AreaGeoID = patient.Province', 'left')
      ->join('areageo areageo2', 'areageo2.AreaGeoID = patient.City', 'left')
      ->where('patient.InternalPID', (int) $InternalPID)
      ->findAll();

    if (empty($rows)) { return null; }

    $patient = $rows[0];
    
    if (method_exists($this, 'transformPatientData')) { $patient = $this->transformPatientData($patient); }
    unset($patient['Address']);
    unset($patient['IdentifierType']);
    unset($patient['Identifier']);
    unset($patient['Comment']);

    $patient = ValueSet::transformLabels([$patient], [
      'Sex' => 'sex',
      'Country' => 'country',
      'Race' => 'race',
      'Religion' => 'religion',
      'Ethnic' => 'ethnic',
      'MaritalStatus' => 'marital_status',
    ])[0];

    $patient['PatIdt'] = null;
    $patient['PatAtt'] = [];
    
    foreach ($rows as $row) {
      if ($row['IdentifierType'] && $row['Identifier'] && !$patient['PatIdt']) {
        $patient['PatIdt'] = [
          'IdentifierType' => $row['IdentifierType'],
          'Identifier'     => $row['Identifier'],
        ];
      }

      if ($row['Address']) {
        $patient['PatAtt'][] = ['Address' => $row['Address']];
      }
    }

    if (empty($patient['PatIdt'])) { $patient['PatIdt'] = null; }
    if (empty($patient['PatAtt'])) { $patient['PatAtt'] = null; }

    return $patient;
  }

  public function createPatient($input) {
    $db = \Config\Database::connect();

    $modelPatAtt = new PatAttModel();
    $modelPatCom = new PatComModel();
    $modelPatIdt = new PatIdtModel();

    // Extract nested data before filtering
    $patIdt = $input['PatIdt'] ?? null;
    $patCom = $input['PatCom'] ?? null;
    $patAtt = $input['PatAtt'] ?? null;

    // Remove nested arrays that don't belong to patient table
    unset($input['PatIdt'], $input['PatCom'], $input['PatAtt']);

    if (!empty($input['Custodian'])) {
        if (is_array($input['Custodian'])) {
            $input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
            if ($input['Custodian'] !== null) {
                $input['Custodian'] = (int) $input['Custodian'];
            }
        }
    }

    if (!empty($input['LinkTo']) && is_array($input['LinkTo'])) {
        $internalPids = array_column($input['LinkTo'], 'InternalPID');
        $input['LinkTo'] = implode(',', $internalPids);
    }
    $input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo'];

    $db->transBegin();

    try {
      $previousData = [];
      $this->insert($input);
      $newInternalPID = $this->getInsertID();
      $this->checkDbError($db, 'Insert patient');
      
      $auditDiff = $this->buildAuditDiff([], $input);
      AuditService::logData(
        'PATIENT_REGISTERED',
        'CREATE',
        'patient',
        (string) $newInternalPID,
        'patient',
        null,
        null,
        null,
        'Patient registration',
        [
          'diff' => $auditDiff,
          'patient_id' => $input['PatientID'] ?? null,
          'validation_profile' => 'patient.create',
        ],
        ['entity_version' => 1]
      );

      if (!empty($patIdt)) {
        $modelPatIdt->createPatIdt($patIdt, $newInternalPID);
        $this->checkDbError($db, 'Insert PatIdt');
      }

      if (!empty($patCom)) {
        $modelPatCom->createPatCom($patCom, $newInternalPID);
        $this->checkDbError($db, 'Insert PatCom');
      }

      if (!empty($patAtt)) {
        $modelPatAtt->createPatAtt($patAtt, $newInternalPID);
        $this->checkDbError($db, 'Insert PatAtt');
      }

      $db->transCommit();

      return $newInternalPID;

    } catch (\Exception $e) {
        $db->transRollback();
        throw $e; 
    }
  }

  public function updatePatient($input) {
    $db = \Config\Database::connect();
    $modelPatIdt = new PatIdtModel();
    $modelPatCom = new PatComModel();
    $modelPatAtt = new PatAttModel();

    if (!empty($input['Custodian'])) {
        if (is_array($input['Custodian'])) {
            $input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
            if ($input['Custodian'] !== null) {
                $input['Custodian'] = (int) $input['Custodian'];
            }
        }
    }

    if (!empty($input['LinkTo']) && is_array($input['LinkTo'])) {
        $internalPids = array_column($input['LinkTo'], 'InternalPID');
        $input['LinkTo'] = implode(',', $internalPids);
    }
    $input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo'];

    $db->transBegin();

    try {
      $InternalPID = $input['InternalPID'];
      $previousData = $this->find($InternalPID) ?? [];
      $this->where('InternalPID',$InternalPID)->set($input)->update();
      $this->checkDbError($db, 'Update patient');
      
      $changedFields = array_keys(array_diff_assoc((array) $previousData, (array) $input));
      $auditDiff = $this->buildAuditDiff((array) $previousData, $input);
      AuditService::logData(
        'PATIENT_DEMOGRAPHICS_UPDATED',
        'UPDATE',
        'patient',
        (string) $InternalPID,
        'patient',
        null,
        null,
        null,
        'Patient data updated',
        [
          'diff' => $auditDiff,
          'changed_fields' => $changedFields,
          'validation_profile' => 'patient.update',
        ],
        ['entity_version' => 1]
      );

      if (!empty($input['PatIdt'])) {
        $modelPatIdt->updatePatIdt($input['PatIdt'], $InternalPID);
        $this->checkDbError($db, 'Update patIdt');
      } else {
        $modelPatIdt->deletePatIdt($InternalPID);
        $this->checkDbError($db, 'Update patidt');
      }

      if (!empty($input['PatCom'])) {
        $modelPatCom->updatePatCom($input['PatCom'], $InternalPID);
        $this->checkDbError($db, 'Update PatCom');
      } else {
        $modelPatCom->deletePatCom($InternalPID);
        $this->checkDbError($db, 'Update patcom');
      }

      if (!empty($input['PatAtt'])) {
        $modelPatAtt->updatePatAtt($input['PatAtt'], $InternalPID);
        $this->checkDbError($db, 'Update PatAtt');
      } else {
        $modelPatAtt->deletePatAtt($InternalPID);
        $this->checkDbError($db, 'Update/Delete patatt');
      }

      $db->transCommit();

      return $InternalPID;

    } catch (\Exception $e) {
        $db->transRollback();
        throw $e; 
    }
  }

  private function transformPatientData(array $patient): array {
    $patient["Age"] = $this->calculateAgeFromBirthdate($patient["Birthdate"], $patient["TimeOfDeath"]);
    $patient["TimeOfDeath"] = $this->formattedDate($patient["TimeOfDeath"]);
    $patient["CreateDate"] = $this->formattedDate($patient["CreateDate"]);
    $patient["BirthdateConversion"] = $this->formatedDateForDisplay($patient["Birthdate"]);
    $patient["LinkTo"]    = $this->getLinkedPatients($patient['LinkTo']);
    $patient["Custodian"] = $this->getCustodian($patient['Custodian']);
    $patient['PatCom'] = $patient['Comment'];

    return $patient;
  }

  private function getLinkedPatients(?string $linkTo): ?array {
    if (empty($linkTo)) { return null; }

    $ids = array_filter(explode(',', $linkTo));

    return $this->db->table('patient')
      ->select('InternalPID, PatientID')
      ->whereIn('InternalPID', $ids)
      ->get()
      ->getResultArray() ?: null;
  }

  private function getCustodian($custodianId): ?array {
    if (empty($custodianId)) {
      return null;
    }

    return $this->select('InternalPID, PatientID')
      ->where('InternalPID', (int) $custodianId)
      ->first() ?: null;
  }

  private function calculateAgeFromBirthdate($birthdate, $deathdate) {
    $dob = new \DateTime($birthdate);

    if ($deathdate == null) {
      $today = new \DateTime();
    } else {
      $deathdate = new \DateTime($deathdate);
      $today = $deathdate;
    }

    $diff = $today->diff($dob);
    $formattedAge = "";
    if ($diff->y > 0){
      $formattedAge .= "{$diff->y} Years ";
    }
    if ($diff->m > 0){
      $formattedAge .= "{$diff->m} Months ";
    }
    if ($diff->d > 0){
      $formattedAge .= "{$diff->d} Days";
    }

    return $formattedAge;
  }

  private function formattedDate(?string $dateString): ?string {
    try {
        if (empty($dateString)) {return null;}

        $dt = new \DateTime($dateString, new \DateTimeZone("UTC"));
        return $dt->format('Y-m-d\TH:i:s\Z');
    } catch (\Exception $e) {
        return null; 
    }
  }

  private function formatedDateForDisplay($dateString) {    
    $date = \DateTime::createFromFormat('Y-m-d H:i', $dateString);

    if (!$date) {
      $timestamp = strtotime($dateString);
      if ($timestamp) {
        return date('j M Y', $timestamp);
      }
      return null;
    }

    return $date->format('j M Y');
  }

  public function updatePatientPartial(int $InternalPID, array $input): ?int {
    $db = \Config\Database::connect();
    $modelPatIdt = new PatIdtModel();
    $modelPatCom = new PatComModel();
    $modelPatAtt = new PatAttModel();

    $patient = $this->find($InternalPID);
    if (!$patient) {
      return null;
    }

    $hasPatIdt = array_key_exists('PatIdt', $input);
    $hasPatCom = array_key_exists('PatCom', $input);
    $hasPatAtt = array_key_exists('PatAtt', $input);

    $patIdt = $hasPatIdt ? $input['PatIdt'] : null;
    $patCom = $hasPatCom ? $input['PatCom'] : null;
    $patAtt = $hasPatAtt ? $input['PatAtt'] : null;

    unset($input['PatIdt'], $input['PatCom'], $input['PatAtt']);

    if (array_key_exists('Custodian', $input)) {
      if (is_array($input['Custodian'])) {
        $input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
      }

      if ($input['Custodian'] !== null && $input['Custodian'] !== '') {
        $input['Custodian'] = (int) $input['Custodian'];
      } else {
        $input['Custodian'] = null;
      }
    }

    if (array_key_exists('LinkTo', $input)) {
      if (is_array($input['LinkTo'])) {
        $internalPids = array_column($input['LinkTo'], 'InternalPID');
        $internalPids = array_filter($internalPids, static fn($pid) => $pid !== null && $pid !== '');
        $input['LinkTo'] = empty($internalPids) ? null : implode(',', $internalPids);
      }
    }

    $allowedMap = array_flip($this->allowedFields);
    $patientUpdate = array_intersect_key($input, $allowedMap);
    unset($patientUpdate['CreateDate'], $patientUpdate['DelDate']);

    $db->transBegin();

    try {
      $previousData = $this->find($InternalPID) ?? [];

      if ($patientUpdate !== []) {
        $this->where('InternalPID', $InternalPID)->set($patientUpdate)->update();
        $this->checkDbError($db, 'Update patient');
      }

      if ($hasPatIdt) {
        if ($patIdt === null || $patIdt === []) {
          $modelPatIdt->deletePatIdt((string) $InternalPID);
          $this->checkDbError($db, 'Delete patidt');
        } else {
          $modelPatIdt->updatePatIdt($patIdt, (string) $InternalPID);
          $this->checkDbError($db, 'Update patidt');
        }
      }

      if ($hasPatCom) {
        if ($patCom === null) {
          $modelPatCom->deletePatCom((string) $InternalPID);
          $this->checkDbError($db, 'Delete patcom');
        } else {
          $modelPatCom->updatePatCom((string) $patCom, (string) $InternalPID);
          $this->checkDbError($db, 'Update PatCom');
        }
      }

      if ($hasPatAtt) {
        if ($patAtt === null) {
          $modelPatAtt->deletePatAtt((string) $InternalPID);
          $this->checkDbError($db, 'Delete patatt');
        } else {
          $modelPatAtt->updatePatAtt((array) $patAtt, (string) $InternalPID);
          $this->checkDbError($db, 'Update/Delete patatt');
        }
      }

      $afterData = array_merge((array) $previousData, $patientUpdate);
      $auditDiff = $this->buildAuditDiff((array) $previousData, $afterData);
      $changedFields = array_column($auditDiff, 'field');

      if ($hasPatIdt) {
        $changedFields[] = 'PatIdt';
      }
      if ($hasPatCom) {
        $changedFields[] = 'PatCom';
      }
      if ($hasPatAtt) {
        $changedFields[] = 'PatAtt';
      }

      AuditService::logData(
        'PATIENT_DEMOGRAPHICS_UPDATED',
        'UPDATE',
        'patient',
        (string) $InternalPID,
        'patient',
        null,
        null,
        null,
        'Patient data updated',
        [
          'diff' => $auditDiff,
          'changed_fields' => array_values(array_unique($changedFields)),
          'validation_profile' => 'patient.patch',
        ],
        ['entity_version' => 1]
      );

      $db->transCommit();

      return $InternalPID;

    } catch (\Exception $e) {
      $db->transRollback();
      throw $e;
    }
  }

  private function buildAuditDiff(array $before, array $after): array {
      $diff = [];
      $fields = array_unique(array_merge(array_keys($before), array_keys($after)));
      foreach ($fields as $field) {
          $prev = $before[$field] ?? null;
          $next = $after[$field] ?? null;
          if ($prev === $next) {
              continue;
          }
          $diff[] = [
              'field' => $field,
              'previous' => $prev,
              'new' => $next,
          ];
      }
      return $diff;
  }

  private function checkDbError($db, string $context) {
      $error = $db->error();
      if (!empty($error['code'])) {
          throw new \Exception(
              "{$context} failed: {$error['code']} - {$error['message']}"
          );
      }
  }

  public function deletePatient($InternalPID) {
    $db = \Config\Database::connect();
    $db->transBegin();
    
    try {
      $previousData = $this->find($InternalPID);
      if (!$previousData) {
        throw new \Exception('Patient not found');
      }
      
      $this->delete($InternalPID);
      $this->checkDbError($db, 'Delete patient');
      
      $auditDiff = $this->buildAuditDiff((array) $previousData, []);
      AuditService::logData(
        'PATIENT_DELETED',
        'DELETE',
        'patient',
        (string) $InternalPID,
        'patient',
        null,
        $previousData,
        null,
        'Patient deleted',
        [
          'diff' => $auditDiff,
          'patient_id' => $previousData['PatientID'] ?? null,
        ],
        ['entity_version' => 1]
      );
      
      $db->transCommit();
      return $InternalPID;
    } catch (\Exception $e) {
      $db->transRollback();
      throw $e;
    }
  }

  private function isValidDateTime($datetime) {
    if (empty($datetime) || $datetime=="") {return null; }
    try {
      if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $datetime)) {
          $dt = \DateTime::createFromFormat('Y-m-d', $datetime);
          return $dt ? $dt->format('Y-m-d') : null;
      }

      $dt = new \DateTime($datetime);
      return $dt->format('Y-m-d H:i:s');

    } catch (\Exception $e) {
      return null;
    }
  }

}
