<?php

namespace App\Services\Admin\Sms;

use App\Repositories\Admin\Sms\AdminSmsBatchRepository;
use App\Services\SmsService;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;

final class AdminSmsService
{
    public function __construct(
        private readonly AdminSmsBatchRepository $repo,
        private readonly SmsService $smsService,
    ) {}

    /**
     * send_mode:
     * - one : 단건 (전화번호만 or "전화번호,치환값..." 가능)
     * - many: 대량 붙여넣기 (1줄=1명, "전화번호" 또는 "전화번호,치환값..." 가능) / 최대 100건(중복 제거 후)
     * - csv : CSV 업로드 (1행=1명, phone + 치환값...)
     */
    public function createBatch(int $adminId, array $data, Request $request): array
    {
        $mode        = (string)($data['send_mode'] ?? 'one');
        $from        = (string)($data['from_number'] ?? '');
        $msgRaw      = (string)($data['message'] ?? '');
        $smsTypeHint = (string)($data['sms_type_hint'] ?? 'auto');

        $scheduleType = (string)($data['schedule_type'] ?? 'now');
        $scheduledAt  = null;

        if ($scheduleType === 'schedule') {
            $scheduledAt = !empty($data['scheduled_at']) ? $data['scheduled_at'] : null;
            if ($scheduledAt === null) {
                return ['ok' => false, 'message' => '예약 시간을 입력해 주세요.'];
            }
        }

        try {
            $targets = match ($mode) {
                'one'  => $this->buildTargetsOne((string)($data['to_number'] ?? ''), $msgRaw),
                'many' => $this->buildTargetsMany((string)($data['to_numbers_text'] ?? ''), $msgRaw),
                'csv'  => $this->buildTargetsCsv($request->file('template_csv'), $msgRaw),
                default => throw new \RuntimeException('지원하지 않는 send_mode 입니다.'),
            };
        } catch (\Throwable $e) {
            return ['ok' => false, 'message' => $e->getMessage()];
        }

        $total   = $targets['total'];
        $valid   = $targets['valid'];
        $invalid = $targets['invalid'];
        $dup     = $targets['duplicate'];
        $items   = $targets['items'];

        if ($valid < 1) {
            return ['ok' => false, 'message' => '유효한 수신번호가 없습니다.'];
        }

        $byteLen = $this->calcBytesCiLike($msgRaw);

        $batchId = $this->repo->insertBatch([
            'admin_user_id'   => $adminId,
            'from_number'     => $from,
            'send_mode'       => $mode,
            'schedule_type'   => $scheduleType,
            'scheduled_at'    => $scheduledAt,

            'message_raw'     => $msgRaw,
            'byte_len'        => $byteLen,
            'sms_type_hint'   => in_array($smsTypeHint, ['auto','sms','mms'], true) ? $smsTypeHint : 'auto',

            'total_count'     => $total,
            'valid_count'     => $valid,
            'duplicate_count' => $dup,
            'invalid_count'   => $invalid,

            'status'          => ($scheduleType === 'schedule') ? 'scheduled' : 'queued',
            'request_ip'      => $request->ip(),
            'user_agent'      => substr((string)$request->userAgent(), 0, 255),
        ]);

        //sms mms 발송
        $this->repo->insertItemsChunked($batchId, $items);
        $this->repo->updateBatch($batchId, ['status' => 'submitting']);

        $submitted = 0;
        $failed = 0;

        foreach ($items as $it) {
            $payload = [
                'from_number' => $from,
                'to_number'   => $it['to_number'],
                'message'     => $it['message_final'],
                'sms_type'    => $it['sms_type'],
                'scheduled_at' => $scheduledAt,
            ];

            $ok = $this->smsService->send($payload, 'lguplus');

            if ($ok) {
                $submitted++;
                $this->repo->updateItemBySeq($batchId, (int)$it['seq'], [
                    'status' => 'submitted',
                    'submitted_at' => now()->format('Y-m-d H:i:s'),
                ]);
            } else {
                $failed++;
                $this->repo->updateItemBySeq($batchId, (int)$it['seq'], [
                    'status' => 'failed',
                    'provider_message' => 'SmsService send() returned false',
                ]);
            }


            $status = 'submitted';
            if ($submitted < 1) $status = 'failed';
            else if ($failed > 0) $status = 'partial';

            $this->repo->updateBatch($batchId, [
                'status' => $status,
                'error_message' => $failed > 0 ? "failed={$failed}" : null,
            ]);
        }

        return [
            'ok' => true,
            'batch_id' => $batchId,
            'total' => $total,
            'valid' => $valid,
            'invalid' => $invalid,
            'duplicate' => $dup,
        ];
    }

    /**
     * 단건: "010..." 또는 "010...,값,값,값..."
     */
    private function buildTargetsOne(string $toRaw, string $message): array
    {
        [$digits, $args] = $this->parseInlineRow($toRaw);

        if (!$this->isValidKrMobile($digits)) {
            return ['total'=>1,'valid'=>0,'invalid'=>1,'duplicate'=>0,'items'=>[]];
        }

        $msgFinal = $this->applyTokensIfNeeded($message, $args, '단건 입력');

        return [
            'total' => 1,
            'valid' => 1,
            'invalid' => 0,
            'duplicate' => 0,
            'items' => [[
                'seq' => 1,
                'to_number' => $digits,
                'message_final' => $msgFinal,
                'sms_type' => $this->guessSmsType($msgFinal),
                'status' => 'queued',
            ]],
        ];
    }

    /**
     * 대량(붙여넣기): 1줄=1명
     * - 전화번호만 가능
     * - 또는 "전화번호,치환값..." 가능 (토큰 치환 지원)
     * - 최대 100건 (중복 제거 후)
     */
    private function buildTargetsMany(string $text, string $message): array
    {
        $lines = preg_split('/\r\n|\n|\r/', (string)$text) ?: [];
        $lines = array_values(array_filter(array_map('trim', $lines), fn($v) => $v !== ''));

        $total = count($lines);
        $invalid = 0;

        // 메시지에 토큰이 있으면 필요 args 수 계산
        $idxs = $this->extractTemplateIndexes($message);
        $needArgs = count($idxs);

        // phone별 첫 줄만 사용(중복 제거)
        $firstByPhone = []; // phone => [args, lineNo]
        $dup = 0;

        foreach ($lines as $i => $line) {
            $lineNo = $i + 1;
            [$phone, $args] = $this->parseInlineRow($line);

            if (!$this->isValidKrMobile($phone)) {
                $invalid++;
                continue;
            }

            // 토큰 사용중이면 이 줄에 치환값이 충분해야 함
            if ($needArgs > 0) {
                $this->assertTokensContiguousFrom2($idxs, "대량 {$lineNo}라인");
                if (count($args) < $needArgs) {
                    throw new \RuntimeException("대량 {$lineNo}라인: 토큰 치환값이 부족합니다. 필요 {$needArgs}개인데 입력은 ".count($args)."개입니다. (형식: 전화번호,값,값,...)");
                }
            }

            if (!isset($firstByPhone[$phone])) {
                $firstByPhone[$phone] = [$args, $lineNo];
            } else {
                $dup++;
            }
        }

        $uniqPhones = array_keys($firstByPhone);

        // 100건 제한(중복 제거 후)
        if (count($uniqPhones) > 100) {
            throw new \RuntimeException('대량(붙여넣기)은 최대 100건입니다. 100건 초과는 CSV 업로드를 이용해 주세요.');
        }

        $items = [];
        $seq = 1;

        foreach ($uniqPhones as $phone) {
            [$args] = $firstByPhone[$phone];

            $msgFinal = $this->applyTokensIfNeeded($message, $args, '대량 붙여넣기');
            $items[] = [
                'seq' => $seq++,
                'to_number' => $phone,
                'message_final' => $msgFinal,
                'sms_type' => $this->guessSmsType($msgFinal),
                'status' => 'queued',
            ];
        }

        return [
            'total' => $total,
            'valid' => count($items),
            'invalid' => $invalid,
            'duplicate' => $dup,
            'items' => $items,
        ];
    }

    /**
     * CSV 업로드: 1행=1명 (phone + 치환값...)
     * - 토큰 없으면 phone만 있어도 OK
     * - 토큰 있으면 컬럼이 부족하면 에러
     */
    private function buildTargetsCsv(?UploadedFile $csv, string $message): array
    {
        if (!$csv instanceof UploadedFile) {
            throw new \RuntimeException('CSV 파일을 업로드해 주세요.');
        }

        $idxs = $this->extractTemplateIndexes($message);
        $needArgs = count($idxs);
        if ($needArgs > 0) {
            $this->assertTokensContiguousFrom2($idxs, 'CSV');
        }

        $total = 0;
        $invalid = 0;

        $firstByPhone = []; // phone => row
        $dup = 0;

        $lineNo = 0;
        foreach ($this->readCsvLines($csv) as $row) {
            $lineNo++;
            if (empty($row)) continue;

            $total++;

            $phone = $this->normalizePhone((string)($row[0] ?? ''));
            if (!$this->isValidKrMobile($phone)) {
                $invalid++;
                continue;
            }

            if ($needArgs > 0) {
                for ($i=0; $i<$needArgs; $i++) {
                    $v = trim((string)($row[$i+1] ?? ''));
                    if ($v === '') {
                        throw new \RuntimeException("CSV {$lineNo}라인: {_text_0".(2+$i)."_} 매칭 값이 없습니다.");
                    }
                }
            }

            if (!isset($firstByPhone[$phone])) {
                $firstByPhone[$phone] = $row;
            } else {
                $dup++;
            }
        }

        $items = [];
        $seq = 1;
        foreach (array_keys($firstByPhone) as $phone) {
            $row = $firstByPhone[$phone];

            $args = [];
            if ($needArgs > 0) {
                for ($i=0; $i<$needArgs; $i++) $args[] = trim((string)$row[$i+1]);
            }

            $msgFinal = $this->applyTokensIfNeeded($message, $args, 'CSV');
            $items[] = [
                'seq' => $seq++,
                'to_number' => $phone,
                'message_final' => $msgFinal,
                'sms_type' => $this->guessSmsType($msgFinal),
                'status' => 'queued',
            ];
        }

        return [
            'total' => $total,
            'valid' => count($items),
            'invalid' => $invalid,
            'duplicate' => $dup,
            'items' => $items,
        ];
    }

    private function parseInlineRow(string $line): array
    {
        $line = trim((string)$line);
        if ($line === '') return ['', []];

        $parts = array_map('trim', explode(',', $line));
        // 빈값 제거는 args의 공백도 날릴 수 있으니 "앞쪽만" 최소 정리
        $parts = array_values($parts);

        $phone = $this->normalizePhone($parts[0] ?? '');
        $args  = array_slice($parts, 1); // {_text_02_}부터 순서대로
        // args는 trim만
        $args  = array_map(fn($v) => trim((string)$v), $args);

        return [$phone, $args];
    }

    private function applyTokensIfNeeded(string $message, array $args, string $label): string
    {
        $idxs = $this->extractTemplateIndexes($message);
        if (empty($idxs)) return $message;

        $this->assertTokensContiguousFrom2($idxs, $label);

        $needArgs = count($idxs);
        if (count($args) < $needArgs) {
            throw new \RuntimeException("{$label}: 토큰 치환값이 부족합니다. 필요 {$needArgs}개인데 입력은 ".count($args)."개입니다.");
        }

        $out = $message;
        for ($i=0; $i<$needArgs; $i++) {
            $token = '{_text_0'.(2+$i).'_}';
            $out = str_replace($token, (string)$args[$i], $out);
        }
        return $out;
    }

    private function assertTokensContiguousFrom2(array $idxs, string $label): void
    {
        $idxs = array_values($idxs);
        sort($idxs);

        if ($idxs[0] !== 2) {
            throw new \RuntimeException("{$label}: 첫 토큰은 {_text_02_}로 시작해야 합니다.");
        }
        for ($i=0; $i<count($idxs); $i++) {
            if ($idxs[$i] !== (2 + $i)) {
                $need = 2 + $i;
                throw new \RuntimeException("{$label}: {_text_0{$need}_} 토큰이 누락되었습니다.");
            }
        }
    }

    private function normalizePhone(string $v): string
    {
        return preg_replace('/\D+/', '', $v) ?? '';
    }

    private function isValidKrMobile(string $digits): bool
    {
        return (bool) preg_match('/^01\d{8,9}$/', $digits);
    }

    private function extractTemplateIndexes(string $message): array
    {
        preg_match_all('/\{_text_(0[2-9])_\}/', $message, $m);
        $raw = array_unique($m[1] ?? []);
        $idxs = array_map(fn($s) => (int)$s, $raw);
        sort($idxs);
        return $idxs;
    }

    private function guessSmsType(string $message): string
    {
        $len = @mb_strlen($message, 'EUC-KR');
        return ($len !== false && $len > 90) ? 'mms' : 'sms';
    }

    private function calcBytesCiLike(string $str): int
    {
        $bytes = 0;
        $len = mb_strlen($str, 'UTF-8');
        for ($i=0; $i<$len; $i++) {
            $ch = mb_substr($str, $i, 1, 'UTF-8');
            $ord = unpack('N', mb_convert_encoding($ch, 'UCS-4BE', 'UTF-8'))[1] ?? 0;
            $bytes += ($ord > 127) ? 2 : 1;
        }
        return $bytes;
    }

    private function readCsvLines(UploadedFile $csv): \Generator
    {
        $path = $csv->getRealPath();
        if ($path === false) return;

        $fh = fopen($path, 'rb');
        if (!$fh) return;

        try {
            while (($row = fgetcsv($fh)) !== false) {
                if ($row === [null] || $row === false) continue;

                $out = [];
                foreach ($row as $cell) {
                    $out[] = $this->toUtf8((string)$cell);
                }
                yield $out;
            }
        } finally {
            fclose($fh);
        }
    }

    private function toUtf8(string $s): string
    {
        $s = preg_replace('/^\xEF\xBB\xBF/', '', $s) ?? $s;
        if (mb_check_encoding($s, 'UTF-8')) return $s;
        $converted = @mb_convert_encoding($s, 'UTF-8', 'CP949,EUC-KR,ISO-8859-1');
        return is_string($converted) ? $converted : $s;
    }
}
