<?php

namespace App\Forms;

use RuntimeException;

/**
 * CommonForm
 * - 모든 Form의 공통 베이스
 * - 핵심 개선점:
 *   1) FK/숫자 필드 미입력('')을 NULL로 정규화 ('' -> null)
 *   2) 전역 null -> '' 변환 제거 (FK/숫자/날짜 타입 깨짐 방지)
 *   3) validate()에서 dynamicRules 누적 버그 수정 (마지막 규칙만 남는 문제 해결)
 *   4) "필드 존재 보장"으로 임의 '' 삽입 제거 (미입력 필드가 FK/숫자 규칙을 깨는 문제 방지)
 *   5) role.* 같은 배열 원소 규칙을 위해 부모 배열 보정 로직 유지/강화
 */
abstract class CommonForm
{
  private $_validation = null;
  private array $_attributes = [];
  private array $_formFields = [];
  private array $_formRules = [];
  private array $_formFilters = [];
  private array $_indexFilters = [];
  private array $_batchjobFilters = [];
  private array $_formOptions = [];
  private array $_actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
  private array $_batchjobButtons = ['batchjob' => '일괄처리', 'batchjob_delete' => '일괄삭제'];

  protected $formAction = null;

  protected function __construct()
  {
    $this->_validation = service('validation');
  }

  public function form_init_process(string $action, array &$formDatas = []): void
  {
    log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Called...");
    $this->formAction = $action;
    $actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
    $batchjobButtons = [];
    $this->setActionButtons($actionButtons);
    $this->setBatchjobButtons($batchjobButtons);
  }

  final public function setAttributes(array $attributes): void
  {
    $this->_attributes = $attributes;
  }

  final public function getAttribute(string $key): string
  {
    if (!array_key_exists($key, $this->_attributes)) {
      throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$key}에 해당하는 속성이 정의되지 않았습니다.");
    }
    return $this->_attributes[$key];
  }

  final public function setFormFields(array $fields): void
  {
    foreach ($fields as $field) {
      $this->_formFields[$field] = $this->getFormFieldLabel($field);
    }
  }

  // $fields 매치된것만 반환, []->전체
  final public function getFormFields(array $fields = []): array
  {
    if (empty($fields)) {
      return $this->_formFields;
    }
    return array_intersect_key($this->_formFields, array_flip($fields));
  }

  public function setFormRules(array $fields, $formRules = []): void
  {
    foreach ($fields as $field) {
      $formRules = $this->getFormRule($field, $formRules);
    }
    $this->_formRules = $formRules;
  }

  final public function getFormRules(array $fields = []): array
  {
    if (empty($fields)) {
      return $this->_formRules;
    }
    return array_intersect_key($this->_formRules, array_flip($fields));
  }

  final public function setFormOptions(array $fields, array $formDatas = [], $formOptions = []): void
  {
    foreach ($fields as $field) {
      $formOptions[$field] = $formOptions[$field] ?? $this->getFormOption($field, $formDatas);
    }
    $this->_formOptions = $formOptions;
  }

  // $fields 매치된것만 반환, []->전체
  final public function getFormOptions(array $fields = []): array
  {
    if (empty($fields)) {
      return $this->_formOptions;
    }
    return array_intersect_key($this->_formOptions, array_flip($fields));
  }

  final public function setFormFilters(array $fields): void
  {
    $this->_formFilters = $fields;
  }

  final public function getFormFilters(): array
  {
    return $this->_formFilters;
  }

  final public function setIndexFilters(array $fields): void
  {
    $this->_indexFilters = $fields;
  }

  final public function getIndexFilters(): array
  {
    return $this->_indexFilters;
  }

  final public function setBatchjobFilters(array $fields): void
  {
    $this->_batchjobFilters = $fields;
  }

  final public function getBatchjobFilters(): array
  {
    return $this->_batchjobFilters;
  }

  final public function setActionButtons(array $buttons): array
  {
    return $this->_actionButtons = $buttons;
  }

  final public function getActionButtons(): array
  {
    return $this->_actionButtons;
  }

  final public function setBatchjobButtons(array $buttons): array
  {
    return $this->_batchjobButtons = $buttons;
  }

  final public function getBatchjobButtons(): array
  {
    return $this->_batchjobButtons;
  }

  /* ---------------------------------------------------------------------
   * Normalize / Sanitize
   * --------------------------------------------------------------------- */

  /**
   * 1) 깊은 배열 구조 정리(배열은 유지)
   * - 여기서는 null -> '' 같은 변환을 절대 하지 않습니다.
   * - 이유: FK/숫자/날짜 필드가 ''로 변하면 validation/DB에서 문제가 발생함.
   */
  protected function sanitizeFormDatas($data, string $path = '')
  {
    if (!is_array($data)) {
      return $data;
    }

    foreach ($data as $k => $v) {
      if (is_array($v)) {
        $data[$k] = $this->sanitizeFormDatas($v, ($path !== '' ? "{$path}.{$k}" : (string) $k));
      }
    }

    return $data;
  }

  /**
   * 3) role.* 같은 배열 원소 규칙이 있을 때, 부모 배열 존재/타입 보정
   */
  protected function ensureParentArrayForWildcardRules(array &$formDatas, array $formRules): void
  {
    foreach ($formRules as $fieldKey => $ruleDef) {
      $fieldName = (string) $fieldKey;

      if (!str_contains($fieldName, '.*')) {
        continue;
      }

      $parent = str_replace('.*', '', $fieldName);

      // 1) 부모가 없거나 ''/null 이면 빈 배열
      if (!array_key_exists($parent, $formDatas) || $formDatas[$parent] === '' || $formDatas[$parent] === null) {
        $formDatas[$parent] = [];
        continue;
      }

      // 2) 문자열이면 CSV로 분해 (혹시 JSON 문자열이면 JSON 우선)
      if (is_string($formDatas[$parent])) {
        $raw = trim($formDatas[$parent]);
        if ($raw === '') {
          $formDatas[$parent] = [];
        } else {
          $decoded = json_decode($raw, true);
          if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            $formDatas[$parent] = $decoded;
          } else {
            $formDatas[$parent] = explode(DEFAULTS["DELIMITER_COMMA"], $raw);
          }
        }
      }

      // 3) 배열이 아니면 강제 빈 배열
      if (!is_array($formDatas[$parent])) {
        $formDatas[$parent] = [];
      }

      // ✅ 4) 핵심: 배열 원소의 null/'' 제거 + 문자열화(Trim이 null 받지 않도록)
      $clean = array_map(
        fn($v) => is_scalar($v) ? trim((string) $v) : '',
        $formDatas[$parent]
      );
      $clean = array_values(array_filter($clean, fn($v) => $v !== ''));

      $formDatas[$parent] = $clean;
    }
  }

  /* ---------------------------------------------------------------------
   * Validation
   * --------------------------------------------------------------------- */

  /**
   * 데이터를 검증하고 유효하지 않을 경우 예외를 발생시킵니다.
   * 2025 CI4 표준: 규칙 배열 내에 label을 포함하여 한글 메시지 출력을 보장합니다.
   */
  public function validate_process(array &$formDatas, $isDebug = false): array
  {
    // 1) 전체 라벨/룰
    $allFormFields = $this->getFormFields();   // ['field' => '라벨', ...]
    $allFormRules = $this->getFormRules();    // ['field' => 'rules', ...]
    if (empty($allFormRules)) {
      throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
    }

    if ($this->formAction === 'modify') {
      // (1) formDatas에 실제로 넘어온 필드만
      $targetFields = array_keys($formDatas);
      if ($isDebug) {
        log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate targetFields Begin DEBUG");
        log_message('debug', var_export($targetFields, true));
      }
      // // (2) 내부 제어용 키 제거(프로젝트에 맞게 추가/삭제)
      // $exclude = ['_method', 'csrf_test_name', 'submit', 'token', 'action'];
      // $targetFields = array_values(array_diff($targetFields, $exclude));
      // // (3) wildcard(role.*) 같은 규칙이 있으면 부모 기반으로 같이 포함
      // //     - formDatas에 role이 있으면 role.* 규칙도 함께 검사되도록 추가
      // foreach ($allFormRules as $ruleField => $_ruleStr) {
      //   $ruleField = (string) $ruleField;
      //   if (!str_contains($ruleField, '.*')) {
      //     continue;
      //   }
      //   $parent = str_replace('.*', '', $ruleField);
      //   if (in_array($parent, $targetFields, true)) {
      //     $targetFields[] = $ruleField; // e.g. 'role.*'
      //   }
      // }
      // // (4) 실제로 룰이 정의된 필드만 남김
      // $targetFields = array_values(array_intersect($targetFields, array_keys($allFormRules)));
      if ($isDebug) {
        log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate targetFields End DEBUG");
        log_message('debug', var_export($targetFields, true));
      }
      // 최종: modify에서는 "타겟만" 룰/라벨 세팅
      $formFields = $this->getFormFields($targetFields);
      $formRules = $this->getFormRules($targetFields);
      // throw new RuntimeException(static::class . "->targetFields" . implode(",", $targetFields) . "\nformRules:" . implode(",", array_keys($formRules)) . "\nformFields:" . implode(",", array_keys($formFields)));
    }//modify
    // 2) 액션별 "검증 대상 필드" 결정
    // $formFields = $allFormFields;
    // $formRules = $allFormRules;
    return array($formFields, $formRules, $allFormFields);
  }
  final public function validate(array &$formDatas, $isDebug = false): void
  {
    log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class . ', formAction:' . $this->formAction);
    try {
      // 0) 데이터 구조 정리 (null 변환 X)
      $formDatas = $this->sanitizeFormDatas($formDatas);
      list($formFields, $formRules, $allFormFields) = $this->validate_process($formDatas, $isDebug);
      if (empty($formRules)) {
        throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 검증할 대상 RULE이 없습니다.");
      }
      // 3) wildcard(role.*) 부모 배열 보정
      $this->ensureParentArrayForWildcardRules($formDatas, $formRules);

      // 5) dynamicRules 구성
      $dynamicRules = [];
      foreach ($formRules as $field => $rule) {
        [$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
        // label 결정
        if (isset($formFields[$fieldName])) {
          $label = $formFields[$fieldName];
        } elseif (str_contains($fieldName, '.*')) {
          $parentField = str_replace('.*', '', $fieldName);
          $label = ($allFormFields[$parentField] ?? $formFields[$parentField] ?? $fieldName) . " 항목";
        } else {
          $label = $fieldName;
        }
        $dynamicRules[$fieldName] = [
          'label' => $label,
          'rules' => $ruleStr,
        ];
      }

      if ($isDebug) {
        log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate dynamicRules DEBUG");
        log_message('debug', var_export($formDatas, true));
        log_message('debug', var_export($dynamicRules, true));
      }

      $this->_validation->setRules($dynamicRules);
      if (!$this->_validation->run($formDatas)) {
        $errors = $this->_validation->getErrors();
        throw new RuntimeException(sprintf(
          "%s->%s작업 필수항목\nformDatas->%s\ndynamicRules->%s\n%s",
          static::class,
          $this->formAction,
          var_export($formDatas, true),
          implode(",", array_keys($dynamicRules)),
          implode("\n", $errors)
        ));
      }

    } catch (\Throwable $e) {
      if ($e instanceof RuntimeException) {
        throw $e;
      }
      // throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
    }
  }

  /* ---------------------------------------------------------------------
   * Overridable hooks
   * --------------------------------------------------------------------- */

  // 사용자 정의 hook: 필드/룰 커스터마이즈
  protected function getValidationRule(string $field, string $rule): array
  {
    return [$field, $rule];
  }

  public function getFormFieldLabel(string $field, ?string $label = null): string
  {
    switch ($field) {
      default:
        $label = $label ?? lang("{$this->getAttribute('class_path')}.label.{$field}");
        break;
    }
    return $label;
  }

  /**
   * Form rule 정의
   * - permit_empty|numeric 인 FK들이 여기서 정의되면,
   *   validate()에서 자동으로 ''->null 정규화 대상에 포함됩니다.
   */
  public function getFormRule(string $field, array $formRules): array
  {
    switch ($field) {
      case $this->getAttribute('pk_field'):
        if (!$this->getAttribute('useAutoIncrement')) {
          $formRules[$field] = sprintf(
            "required|regex_match[/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/]%s",
            in_array($this->formAction, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
          );
        } else {
          $formRules[$field] = "required|numeric";
        }
        break;
      case $this->getAttribute('title_field'):
        $formRules[$field] = sprintf(
          "required|trim|string%s",
          in_array($this->formAction, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
        );
        break;
      case "code":
        $formRules[$field] = sprintf(
          "required|regex_match[/^[a-zA-Z0-9가-힣\-\_]+$/]|min_length[4]%s",
          in_array($this->formAction, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
        );
        break;
      case "user_uid":
        $formRules[$field] = "required|numeric";
        break;
      case "status":
        $formRules[$field] = "required|trim|string";
        break;
      case 'picture':
        $formRules[$field] = "is_image[{$field}]|mime_in[{$field},image/jpg,image/jpeg,image/gif,image/png,image/webp]|max_size[{$field},300]|max_dims[{$field},2048,768]";
        break;
      case "updated_at":
      case "created_at":
      case "deleted_at":
        $formRules[$field] = "permit_empty|trim|valid_date";
        break;
      default:
        $formRules[$field] = "permit_empty|trim|string";
        break;
    }

    return $formRules;
  }

  /* ---------------------------------------------------------------------
   * Options
   * --------------------------------------------------------------------- */

  protected function getFormOption_process($service, string $field, array $formDatas = []): array
  {
    $entities = [];

    switch ($field) {
      default:
        if (in_array($this->formAction, ['create_form', 'modify_form', 'alternative_create_form'])) {
          if (array_key_exists($field, $formDatas)) {
            $where = sprintf("status = '%s' OR %s='%s'", STATUS['AVAILABLE'], $this->getAttribute('pk_field'), $formDatas[$field]);
          } else {
            $where = sprintf("status = '%s'", STATUS['AVAILABLE']);
          }
          $entities = $service->getEntities([$where => null]);
        } else {
          $entities = $service->getEntities();
        }
        break;
    }

    return $entities;
  }

  public function getFormOption(string $field, array $formDatas = [], array $options = ['options' => [], 'atttributes' => []]): array
  {
    $tempOptions = ['' => lang("{$this->getAttribute('class_path')}.label.{$field}") . " 선택"];

    switch ($field) {
      case 'user_uid':
        foreach ($this->getFormOption_process(service('userservice'), $field, $formDatas) as $entity) {
          $tempOptions[$entity->getPK()] = $entity->getTitle();
        }
        $options['options'] = $tempOptions;
        break;

      case 'clientinfo_uid':
        foreach ($this->getFormOption_process(service('customer_clientservice'), $field, $formDatas) as $entity) {
          $tempOptions[$entity->getPK()] = $entity->getCustomTitle();
        }
        $options['options'] = $tempOptions;
        break;

      default:
        $optionDatas = lang($this->getAttribute('class_path') . "." . strtoupper($field));
        if (!is_array($optionDatas)) {
          throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:{$field}가 배열값이 아닙니다.");
        }
        foreach ($optionDatas as $key => $label) {
          $tempOptions[$key] = $label;
        }
        $options['options'] = $tempOptions;
        break;
    }

    return $options;
  }
}
