<?php

declare(strict_types=1);

namespace Acms\Services\Field;

/**
 * $_FILES と POST のアップロードメタを、候補（1 ファイル = 1 候補）の配列へ正規化する。
 *
 * image 型 / file 型で完全に同じだった前処理を一本化する。
 * 移設前は両ブランチに同一のコードが写経されており（実測で 31 文が完全一致）、
 * 片方だけ直すと不整合になる状態だった。
 */
class UploadedFilesNormalizer
{
    /**
     * $_FILES の単一形式 / 配列形式の差を吸収し、候補のリストへ変換する。
     *
     * HTML の `name="photo"` は単一、`name="photo[]"` は配列で $_FILES に入る。
     * 呼び出し側は候補数（count）だけを見れば両方を同じループで扱える。
     *
     * @param array<string, mixed> $file $_FILES[$fieldKey]
     * @return list<array{_tmp_name: string, _name: string}>
     */
    public function candidates(array $file): array
    {
        $candidates = [];
        if (!isset($file['tmp_name'])) {
            return $candidates;
        }
        if (!is_array($file['tmp_name'])) {
            return [
                [
                    '_tmp_name' => (string)$file['tmp_name'],
                    '_name' => (string)($file['name'] ?? ''),
                ],
            ];
        }
        foreach ($file['tmp_name'] as $i => $tmpName) {
            $candidates[] = [
                '_tmp_name' => (string)$tmpName,
                '_name' => (string)($file['name'][$i] ?? ''),
            ];
        }
        return $candidates;
    }

    /**
     * 候補の `old` / `filename` を安全な形へ整え、拒否すべき候補を判定する。
     *
     * 処理内容（移設前の image / file 両ブランチで同一だったもの）:
     *  - `@rawfilename` 指定を「年月/元のファイル名」へ展開する（それ以外の `@...` は空にする）
     *  - null バイト混入対策として、先頭に空白等が付いた値を拒否する
     *  - `../` の連続をパスから除去する（ディレクトリトラバーサル対策の半正規化）
     *  - ファイル名が `.htaccess` で終わる候補を拒否する
     *
     * Why not 候補の配列をまるごと受け渡さない: 候補の配列は image / file で持つキーが違う
     * （image は width・largeWidth・tinyWidth・squareWidth 等、file は fileSize・downloadName 等）。
     * 配列形状を引数の型に書くと呼び出し側の推論がその形へ狭まり、他のキーが「存在しない」と
     * 誤検出される。実際に触る 3 つの値だけを受け取る純粋関数にする。
     *
     * @param string $old 既存ファイルの相対パス（POST の `{fd}@old`）
     * @param string $filename ファイル名オプション（POST の `{fd}@filename`）
     * @param string $uploadName アップロードされた実ファイル名（$_FILES の name）
     * @return array{old: string, filename: string}|null
     *   整えた値。拒否すべき場合は null（呼び出し側はこの候補の処理をスキップする）
     */
    public function sanitizePathOptions(string $old, string $filename, string $uploadName): ?array
    {
        //-------------
        // rawfilename
        if (preg_match('/^@(.*)$/', $filename, $match)) {
            $filename = ('rawfilename' === $match[1])
                ? date('Ym') . '/' . $uploadName
                : '';
        }

        //------------------------------------
        // security check ( nullバイトチェック )
        if ($old !== ltrim($old)) {
            return null;
        }
        if ($filename !== ltrim($filename)) {
            return null;
        }

        //-------------------------------------------------------------
        // パスの半正規化 ( directory traversal対策・バイナリセーフ関数を使用 )
        // この時点で //+ や ^/ は 混入する可能性はあるが無害とみなす
        // preg_replace は正規表現エラー時のみ null を返す。移設前は null をそのまま代入して
        // 後段の文字列連結で '' として扱われていたため、ここで空文字へ倒して等価に保つ。
        $old = (string)preg_replace('/\.+\/+/', '', $old);
        $filename = (string)preg_replace('/\.+\/+/', '', $filename);

        //---------------------------------------------
        // 例外的無視ファイル
        // pathの終端（ファイル名）が特定の場合にリジェクトする
        if (!!preg_match('/\.htaccess$/', $filename)) {
            return null;
        }

        return ['old' => $old, 'filename' => $filename];
    }
}
