<?php

declare(strict_types=1);

namespace App\Models\Traits;

/**
 * 모델 CRUD(생성/수정/삭제) 자동 감사 로그 트레잇.
 *
 * 모델에 `use \App\Models\Traits\Auditable;` 만 추가하면
 * insert/update/delete 시 activity_log 에 "누가·무엇을·언제" 자동 기록한다.
 * (조회(select)는 기록하지 않음)
 *
 * - 변경 전(before)/후(after) 데이터를 JSON 으로 보관
 * - 비밀번호·OTP 비밀키·이메일·전화 등 민감 필드는 마스킹
 * - 기록 실패는 audit_log() 내부에서 흡수되어 본 로직에 영향 없음
 */
trait Auditable
{
    /** 변경/삭제 전 원본 보관 (PK => row) */
    protected array $auditBeforeRows = [];

    /**
     * 모델 이벤트(콜백) 등록.
     * BaseModel 이 이미 $afterInsert 등 속성을 선언하므로(속성 충돌 방지),
     * 트레잇에서는 속성 대신 initialize()에서 런타임에 콜백을 덧붙인다.
     */
    protected function initialize()
    {
        $this->allowCallbacks = true;
        $this->afterInsert[]  = 'auditAfterInsert';
        $this->beforeUpdate[] = 'auditBeforeUpdate';
        $this->afterUpdate[]  = 'auditAfterUpdate';
        $this->beforeDelete[] = 'auditBeforeDelete';
        $this->afterDelete[]  = 'auditAfterDelete';
    }

    /** 마스킹 대상 필드명 */
    protected array $auditMaskFields = ['mb_passwd', 'mb_totp_secret', 'mb_session_token', 'mb_email', 'mb_phone'];

    /**
     * 이 필드들'만' 변경된 update는 활동 로그를 남기지 않는다.
     * (예: 로그인/세션 갱신 같은 시스템 내부 변경 — 노이즈 제거)
     * 모델에서 이 메서드를 override 해 사용. (프로퍼티로 두면 trait/class 비호환 오류)
     *
     * @return list<string>
     */
    protected function auditSkipUpdateOnlyFields(): array
    {
        return [];
    }

    private function auditMask(?array $row): ?array
    {
        if ($row === null) {
            return null;
        }
        foreach ($this->auditMaskFields as $f) {
            if (array_key_exists($f, $row) && (string) $row[$f] !== '') {
                $row[$f] = '***';
            }
        }

        return $row;
    }

    /** find 콜백 재귀를 피하려고 쿼리빌더로 원본 행을 직접 조회 */
    private function auditFetchRaw(int|string $id): ?array
    {
        try {
            $row = $this->db->table($this->table)
                ->where($this->primaryKey, $id)
                ->get()
                ->getRowArray();

            return $row ?: null;
        } catch (\Throwable $e) {
            return null;
        }
    }

    public function auditAfterInsert(array $eventData): array
    {
        helper('audit');
        $after = is_array($eventData['data'] ?? null) ? $eventData['data'] : null;
        audit_log('create', $this->table, (int) ($eventData['id'] ?? 0), null, $this->auditMask($after));

        return $eventData;
    }

    public function auditBeforeUpdate(array $eventData): array
    {
        $this->auditBeforeRows = [];
        $ids = $eventData['id'] ?? null;
        if ($ids !== null) {
            foreach ((array) $ids as $one) {
                $row = $this->auditFetchRaw($one);
                if ($row !== null) {
                    $this->auditBeforeRows[(string) $one] = $row;
                }
            }
        }

        return $eventData;
    }

    public function auditAfterUpdate(array $eventData): array
    {
        helper('audit');
        $ids     = $eventData['id'] ?? null;
        $changes = is_array($eventData['data'] ?? null) ? $eventData['data'] : [];

        // 지정 필드만 변경된 경우(로그인/세션 갱신 등) → 기록 생략
        $skipOnly = $this->auditSkipUpdateOnlyFields();
        if ($skipOnly !== [] && $changes !== []) {
            $changedKeys = array_values(array_diff(array_keys($changes), [$this->primaryKey]));
            if ($changedKeys !== [] && array_diff($changedKeys, $skipOnly) === []) {
                $this->auditBeforeRows = [];

                return $eventData;
            }
        }

        if ($ids !== null) {
            foreach ((array) $ids as $one) {
                $before = $this->auditBeforeRows[(string) $one] ?? null;
                $after  = $before !== null ? array_merge($before, $changes) : $changes;
                audit_log('update', $this->table, (int) $one, $this->auditMask($before), $this->auditMask($after));
            }
        } else {
            // 조건(where) 기반 일괄 수정: 대상 PK 미상 → 변경값만 기록
            audit_log('update', $this->table, 0, null, $this->auditMask($changes));
        }
        $this->auditBeforeRows = [];

        return $eventData;
    }

    public function auditBeforeDelete(array $eventData): array
    {
        $this->auditBeforeRows = [];
        $ids = $eventData['id'] ?? null;
        if ($ids !== null) {
            foreach ((array) $ids as $one) {
                $row = $this->auditFetchRaw($one);
                if ($row !== null) {
                    $this->auditBeforeRows[(string) $one] = $row;
                }
            }
        }

        return $eventData;
    }

    public function auditAfterDelete(array $eventData): array
    {
        helper('audit');
        $ids = $eventData['id'] ?? null;
        if ($ids !== null) {
            foreach ((array) $ids as $one) {
                $before = $this->auditBeforeRows[(string) $one] ?? null;
                audit_log('delete', $this->table, (int) $one, $this->auditMask($before), null);
            }
        } else {
            audit_log('delete', $this->table, 0, null, null);
        }
        $this->auditBeforeRows = [];

        return $eventData;
    }
}
