<?php

namespace App\Models;
use Exception;

class SmsLib
{
    public string $token;
    public string $host;
    public string $port;
    public string $id;
    public string $pw;
    public string $callNum;
    public array $data;
    public array $error;
    public array $history; // 문자 전송 내역 정보
    public array $result; // 문자 전송 결과 정보
    public int $total; // 전송 횟수
    public int $success; // 성공 횟수
    public int $failed; // 실패 횟수
    public array $message; // 처리 결과

    public function __construct()
    {
        $configModel = new Config();

        $this->token = $configModel->item('sms_icode_token', env('SMS_TOKEN'));
        $this->host = $configModel->item('sms_icode_host', env('SMS_IP'));
        $this->port = $configModel->item('sms_icode_port', env('SMS_PORT'));
        $this->id = $configModel->item('sms_icode_id', env('SMS_ID'));
        $this->pw = $configModel->item('sms_icode_pw', env('SMS_PW'));
        $this->callNum = $configModel->item('sms_icode_call_num', env('SMS_NUMBER'));

        $this->_init();
    }

    /**
     * 초기화
     */
    private function _init(): void
    {
        $this->data = [];
        $this->error = [];
        $this->history = [];
        $this->result = [];
        $this->message = [];
        $this->total = 0;
        $this->success = 0;
        $this->failed = 0;
    }

    /**
     * 아이코드 계정 정보 조회
     */
    public function info(): array
    {
        $userInfo = explode(';', getSock("http://www.icodekorea.com/res/userinfo.php?userid={$this->id}&userpw={$this->pw}"));

        return [
            'code' => $userInfo[0], // 결과코드
            'coin' => $userInfo[1], // 고객 잔액 (충전제만 해당)
            'gpay' => $userInfo[2], // 고객의 건수 별 차감액 표시 (충전제만 해당)
            'payment' => $userInfo[3] // 요금제 표시, A:충전제, C:정액제
        ];
    }

    /**
     * 전송 패킷 생성
     * Add(수신번호목록(배열), 발신번호, 전송내용(2000자이내), 제목(옵션, 30자이내), 예약일자(옵션, 12자리)
     */
    private function _add($sendNumber = [], $sendData = []): bool
    {
        $this->_init();

        $userID = ($sendData['userID'] ?? null);
        $subject = ($sendData['subject'] ?? null);
        $content = ($sendData['content'] ?? null);
        $isReserve = ($sendData['isReserve'] ?? 0);
        $reserveAt = ($sendData['reserveAt'] ?? null);
        $callBack = $this->callNum;

        if(!$userID) {
            $this->error[] = '발신 회원번호가 존재하지 않습니다.';
            return false;
        }

        // 내용 개행치환
        $content = preg_replace("/\r\n/", "\n", $content);
        $content = preg_replace("/\r/", "\n", $content);

        // 문자 타입별 Port 설정.
        $sendType = (strlen($content) > 90 ? 1 : 2); // 0: SMS / 1: LMS
        if($sendType == 1) { // LMS일 경우 제목이 있고 SMS면 제목을 앞에 붙여준다.
            $subject = "";
        }else{
            $content = ($subject . ' ' . $content);
        }

        // 예약날짜 형식 변환
        if($isReserve) {
            $reserveAt = date("YmdHi", strtotime($reserveAt));
        }

        $callBack = getTelNumber($callBack, 0);
        $callBack = $this->_cutChar($callBack, 12); // 회신번호

        /** LMS 제목 **/
        /*
        제목필드의 값이 없을 경우 단말기 기종및 설정에 따라 표기 방법이 다름
        1. 설정에서 제목필드보기 설정 Disable -> 제목필드값을 넣어도 미표기
        2. 설정에서 제목필드보기 설정 Enable  -> 제목을 넣지 않을 경우 제목없음으로 자동표시

        제목의 첫글자에 "<",">", 개행문자가 있을경우 단말기종류 및 통신사에 따라 메세지 전송실패 -> 글자를 체크하거나 취환처리요망
        $strSubject = str_replace("<br/>", " ", $strSubject);
        $strSubject = str_replace("<", "[", $strSubject);
        $strSubject = str_replace(">", "]", $strSubject);
        */

        $subject = $this->_cutCharUtf8($subject, 30);
        $content = $this->_cutCharUtf8($content, 2000);

        /* 필수 항목에 대해 정상적인 코드인지 검사 과정. (개발 방식에 따라 활용) */
        $this->error[] = $this->_checkCommonTypeDest($sendNumber); // 번호 검사
        $this->error[] = $this->_isVaildCallback($callBack);
        $this->error[] = $this->_checkCommonTypeDate($reserveAt);

        if(count($this->errors()) > 0) {
            return false;
        }

        $today = now();
        $alreadyTels = []; // 중복 번호 관리
        foreach ($sendNumber as $uid => $tel) {
            if (empty($tel)) {
                continue;
            }
            $tel = getTelNumber($tel, 1);

            if(in_array($tel, $alreadyTels)) {
                continue;
            }else{
                $alreadyTels[] = $tel;
            }

            $list = [
                "key" => $this->token,
                "tel" => $tel,
                "cb" => $callBack,
                "msg" => $content
            ];
            if (!empty($subject)) {
                $list['title'] = $subject;
            }
            if (!empty($reserveAt)) {
                $list['date'] = $reserveAt;
            }
            $packet = json_encode($list);
            $this->data[$uid] = '06' . str_pad(strlen($packet), 4, "0", STR_PAD_LEFT) . $packet;

            $this->history[$uid] = [
                'user_id' => ($uid ?: null),
                'type' => $sendType,
                'subject' => $subject,
                'content' => $content,
                'tel' => $tel,
                'is_reserve' => $isReserve,
                'reserve_at' => $reserveAt,
                'code' => null,
                'message' => null,
                'created_at' => $today,
            ];
        }

        // 전송 결과 저장
        $this->result['user_id'] = $userID;
        $this->result['subject'] = $subject;
        $this->result['content'] = $content;
        $this->result['reserve_at'] = $reserveAt;

        return true;
    }

    /**
     * 문자 실제 전송 처리
     */
    private function _push(): bool
    {
        $fSocket = fsockopen($this->host, $this->port, $errorNo, $errorStr, 2);
        if (!$fSocket) {
            return false;
        }
        set_time_limit(300);

        $gets = "";
        foreach ($this->data as $i => $puts) {
            fputs($fSocket, $puts, strlen($puts));
            while (!$gets) {
                $gets = fgets($fSocket, 32);
            }

            preg_match("/\"cb\":\"([0-9]*)\"/", substr($puts, 6), $matches);
            $tel = $matches[1];

            $resultCode = substr($gets, 6, 2);
            if ($resultCode == '00' || $resultCode == '17') { // 17은 접수(전송)대기.
                $this->message[$i] = ($resultCode . ":" . substr($gets, 8, 12) . ":" . substr($gets, 20, 11));
                $this->success++;
            } else {
                $this->message[$i] = ($resultCode . ":" . $tel . ":Error(" . $resultCode . ")");
                $this->failed++;
                if ($resultCode >= "80") {
                    break;
                }
            }
            $gets = "";
        }

        fclose($fSocket);
        return true;
    }

    /**
     * 전송 결과 생성
     * $this->message 에 메시지 전송 데이터가 `:` 콜론 형태로 저장되어있음
     */
    public function report(): string
    {
        $resultMessage = "";
        if ($this->message) {
            $resultMessage .= "서버에 접속했습니다.<br/><br/>";
            foreach ($this->message as $i => $result) {
                list($code, $phone, $seq) = explode(":", $result);
                if (substr($seq, 0, 5) == "Error") {
                    $resultCode = substr($code, 6, 2);
                    echo $phone . ' 전송오류 (' . $resultCode . '): ';
                    switch (substr($code, 6, 2)) {
                        case '23': // "23:데이터오류, 전송날짜오류, 발신번호미등록"
                            $resultMessage .= "데이터를 다시 확인해 주시기바랍니다.<br/>";
                            break;

                        // 아래의 사유들은 전송진행이 중단됨.
                        case '85': // "85:전송번호 미등록"
                            $resultMessage .=  "등록되지 않는 전송번호 입니다.<br/>";
                            break;
                        case '87': // "87:인증실패"
                            $resultMessage .=  "(정액제-계약확인)인증 받지 못하였습니다.<br/>";
                            break;
                        case '88': // "88:연동모듈 전송불가"
                            $resultMessage .=  "연동모듈 사용이 불가능합니다. 아이코드로 문의하세요.<br/>";
                            break;
                        case '96': // "96:토큰 검사 실패"
                            $resultMessage .=  "사용할 수 없는 토큰키입니다.<br/>";
                            break;
                        case '97': // "97:잔여코인부족"
                            $resultMessage .=  "잔여코인이 부족합니다.<br/>";
                            break;
                        case '98': // "98:사용기간만료"
                            $resultMessage .=  "사용기간이 만료되었습니다.<br/>";
                            break;
                        case '99': // "99:인증실패"
                            $resultMessage .=  "서비스 사용이 불가능합니다. 아이코드로 문의하세요.<br/>";
                            break;
                        default: // "미 확인 오류"
                            $resultMessage .=  "알 수 없는 오류로 전송이 실패하었습니다.<br/>";
                            break;
                    }

                    $this->error[] = $resultMessage;
                } else {
                    $resultCode = substr($code, 0, 2);
                    switch ($resultCode) {
                        case '17':   // "17: 접수(전송)대기 처리. 지연해소시 전송됨."
                            $resultMessage .=  "접수(전송)대기처리 되었습니다.";
                            break;
                        default:   // "00: 전송완료."
                            $resultMessage .=  "전송되었습니다.<br/>";
                            break;
                    }
                    $resultMessage .=  $phone . "로 (msg seq : ' . $seq . ')<br/>";
                }
                $this->total++;
                $this->history[$i]['code'] = $resultCode;
                $this->history[$i]['message'] = addslashes($resultMessage);
            }

            $resultMessage .=  "<br/> 전체(" . $this->total . "건) / 전송완료(" . $this->success . "건) / 전송실패(" . $this->failed . "건)<br/>";

            // 전송 결과 저장
            $this->result['call_num'] = $this->callNum;
            $this->result['total_count'] = $this->total;
            $this->result['success_count'] = $this->success;
            $this->result['failed_count'] = $this->failed;
            $this->result['message'] = '관리자 직접 전송 ' . $resultMessage;
            $this->result['created_at'] = now();
        }
        return $resultMessage;
    }

    /**
     * 전송하기
     */
    public function send($sendNumber = [], $sendData = []): bool
    {
        try {

            // 문자 전송 목록에 추가
            if(!$this->_add($sendNumber, $sendData)) {
                throw new Exception();
            }

            // 문자 전송
            $this->_push();

            // 전송 결과 생성
            $this->report();

            // DB 저장
            $this->_save();

            return true;
        }catch(Exception $e) {
            $this->error[] = $e->getMessage();
            return false;
        }
    }

    /**
     * 전송 자료 저장
     */
    private function _save(): void
    {
        // 문자 전송 내역 저장
        $keys = (new SmsHistory)->setHistory($this->history);

        // 문자 전송 결과 저장
        $this->result['start_id'] = min($keys);
        $this->result['end_id'] = max($keys);

        (new SmsResult)->insert($this->result);
    }

    /**
     * 오류 조회
     */
    public function errors(): array
    {
        return array_filter($this->error);
    }

    /**
     * 원하는 문자열의 길이를 원하는 길이만큼 공백을 넣어 맞추도록 합니다.
     */
    private function _fillSpace(string $text, int $size): string
    {
        for ($i = 0; $i < $size; $i++) {
            $text .= " ";
        }

        return substr($text, 0, $size);
    }

    /**
     * 원하는 문자열을 원하는 길에 맞는지 확인해서 조정하는 기능을 합니다.
     */
    private function _cutChar(string $word, int $cut): string
    {
        $word = substr($word, 0, $cut); // 필요한 길이만큼 취함.
        for ($k = $cut - 1; $k > 1; $k--) {
            if (ord(substr($word, $k, 1)) < 128) { // 한글값은 160 이상.
                break;
            }
        }

        return substr($word, 0, $cut - ($cut - $k + 1) % 2);;
    }

    private function _cutCharUtf8(string $word, int $cut): string
    {
        preg_match_all('/[\xE0-\xFF][\x80-\xFF]{2}|./', $word, $match); // target for BMP

        $m = $match[0];
        $slen = strlen($word); // length of source string
        if ($slen <= $cut) {
            return $word;
        }

        $ret = [];
        $count = 0;
        for ($i = 0; $i < $cut; $i++) {
            $count += (strlen($m[$i]) > 1) ? 2 : 1;
            if ($count > $cut) {
                break;
            }
            $ret[] = $m[$i];
        }

        return join('', $ret);
    }

    /**
     * 잘못된 수신번호 목록을 리턴합니다.
     */
    private function _checkCommonTypeDest(array $strTelList): string
    {
        $result = '';
        foreach ($strTelList as $tel) {
            $tel = preg_replace("/[^0-9]/", "", $tel);
            if (!preg_match("/^(0[173][0136789])([0-9]{3,4})([0-9]{4})$/", $tel)) {
                $result .= $tel . ',';
            }
        }
        return $result;
    }

    /**
     * 회신번호 유효성 여부조회
     * 한국인터넷진흥원 권고사항
     */
    private function _isVaildCallback(string $callback): string
    {
        $_callback = preg_replace('/[^0-9]/', '', $callback);
        if (!preg_match("/^(02|0[3-6]\d|01(0|1|3|5|6|7|8|9)|070|080|007)\-?\d{3,4}\-?\d{4,5}$/", $_callback) &&
            !preg_match("/^(15|16|18)\d{2}\-?\d{4,5}$/", $_callback)) {
            return "회신번호오류";
        }
        if (preg_match("/^(02|0[3-6]\d|01(0|1|3|5|6|7|8|9)|070|080)\-?0{3,4}\-?\d{4}$/", $_callback)) {
            return "회신번호오류";
        }
        return '';
    }

    /**
     * 문자열을 JSON 사용가능 타입으로 변환한다.
     */
    private function _escapeJsonString(string $value): string
    {
        $escapers = ['\\', '"'];
        $replacements = ['\\\\', '\"'];
        return str_replace($escapers, $replacements, $value);
    }

    /**
     * 예약날짜의 값이 정확한 값인지 확인합니다.
     */
    private function _checkCommonTypeDate(?string $strDate): string
    {
        $strDate = preg_replace("/[^0-9]/", "", $strDate);
        if ($strDate) {
            if (strlen($strDate) != 12) {
                return '예약날짜오류';
            }
            if (!checkdate(substr($strDate, 4, 2), substr($strDate, 6, 2), substr($strDate, 0, 4))) {
                return "예약날짜오류";
            }
            if (substr($strDate, 8, 2) > 23 || substr($strDate, 10, 2) > 59) {
                return "예약시간오류";
            }
        }
        return '';
    }
}
