<?php

namespace App\Http\Controllers\Barobill;

use App\Http\Controllers\Controller;
use App\Models\Barobill\AccountCode;
use App\Models\Barobill\BankSyncStatus;
use App\Models\Barobill\BankTransaction;
use App\Models\Barobill\BankTransactionOverride;
use App\Models\Barobill\BankTransactionSplit;
use App\Models\Barobill\BarobillConfig;
use App\Models\Barobill\BarobillMember;
use App\Models\Finance\TradingPartner;
use App\Models\Tenants\Tenant;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * 바로빌 계좌 입출금내역 조회 컨트롤러
 */
class EaccountController extends Controller
{
    /**
     * 바로빌 SOAP 설정
     */
    private ?string $certKey = null;

    private ?string $corpNum = null;

    private bool $isTestMode = false;

    private ?string $soapUrl = null;

    private ?\SoapClient $soapClient = null;

    // 바로빌 파트너사 (본사) 테넌트 ID
    private const HEADQUARTERS_TENANT_ID = 1;

    public function __construct()
    {
        // DB에서 활성화된 바로빌 설정 조회
        $activeConfig = BarobillConfig::where('is_active', true)->first();

        if ($activeConfig) {
            $this->certKey = $activeConfig->cert_key;
            $this->corpNum = $activeConfig->corp_num;
            $this->isTestMode = $activeConfig->environment === 'test';
            // 계좌 조회는 BANKACCOUNT.asmx 사용
            $baseUrl = $this->isTestMode
                ? 'https://testws.baroservice.com'
                : 'https://ws.baroservice.com';
            $this->soapUrl = $baseUrl.'/BANKACCOUNT.asmx?WSDL';
        } else {
            $this->isTestMode = config('services.barobill.test_mode', true);
            // 테스트 모드에 따라 적절한 CERT_KEY 선택
            $this->certKey = $this->isTestMode
                ? config('services.barobill.cert_key_test', '')
                : config('services.barobill.cert_key_prod', '');
            $this->corpNum = config('services.barobill.corp_num', '');
            $this->soapUrl = $this->isTestMode
                ? 'https://testws.baroservice.com/BANKACCOUNT.asmx?WSDL'
                : 'https://ws.baroservice.com/BANKACCOUNT.asmx?WSDL';
        }

        $this->initSoapClient();
    }

    /**
     * SOAP 클라이언트 초기화
     */
    private function initSoapClient(): void
    {
        if (! empty($this->certKey) || $this->isTestMode) {
            try {
                $context = stream_context_create([
                    'ssl' => [
                        'verify_peer' => false,
                        'verify_peer_name' => false,
                        'allow_self_signed' => true,
                    ],
                ]);

                $this->soapClient = new \SoapClient($this->soapUrl, [
                    'trace' => true,
                    'encoding' => 'UTF-8',
                    'exceptions' => true,
                    'connection_timeout' => 30,
                    'stream_context' => $context,
                    'cache_wsdl' => WSDL_CACHE_NONE,
                ]);
            } catch (\Throwable $e) {
                Log::error('바로빌 계좌 SOAP 클라이언트 생성 실패: '.$e->getMessage());
            }
        }
    }

    /**
     * 계좌 입출금내역 메인 페이지
     */
    public function index(Request $request): View|Response
    {
        if ($request->header('HX-Request')) {
            return response('', 200)->header('HX-Redirect', route('finance.account-transactions'));
        }

        // 현재 선택된 테넌트 정보
        $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
        $currentTenant = Tenant::find($tenantId);

        // 해당 테넌트의 바로빌 회원사 정보
        $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();

        // 테넌트별 서버 모드 적용 (회원사 설정 우선)
        $isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;

        // 서버 모드에 따라 SOAP 설정 재초기화
        if ($barobillMember && $barobillMember->server_mode) {
            $this->applyMemberServerMode($barobillMember);
        }

        return view('barobill.eaccount.index', [
            'certKey' => $this->certKey,
            'corpNum' => $this->corpNum,
            'isTestMode' => $isTestMode,
            'hasSoapClient' => $this->soapClient !== null,
            'currentTenant' => $currentTenant,
            'barobillMember' => $barobillMember,
        ]);
    }

    /**
     * 회원사 서버 모드에 따라 SOAP 설정 적용
     */
    private function applyMemberServerMode(BarobillMember $member): void
    {
        $memberTestMode = $member->isTestMode();
        $targetEnv = $memberTestMode ? 'test' : 'production';

        // 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
        $config = BarobillConfig::where('environment', $targetEnv)->first();

        if ($config) {
            $this->isTestMode = $memberTestMode;
            $this->certKey = $config->cert_key;
            $this->corpNum = $config->corp_num;
            $baseUrl = $config->base_url ?: ($memberTestMode
                ? 'https://testws.baroservice.com'
                : 'https://ws.baroservice.com');
            $this->soapUrl = $baseUrl.'/BANKACCOUNT.asmx?WSDL';

            // SOAP 클라이언트 재초기화
            $this->initSoapClient();

            Log::info('[Eaccount] 서버 모드 적용', [
                'targetEnv' => $targetEnv,
                'certKey' => substr($this->certKey ?? '', 0, 10).'...',
                'corpNum' => $this->corpNum,
                'soapUrl' => $this->soapUrl,
            ]);
        } else {
            Log::warning('[Eaccount] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
        }
    }

    /**
     * 등록된 계좌 목록 조회 (GetBankAccountEx)
     */
    public function accounts(Request $request): JsonResponse
    {
        try {
            $availOnly = $request->input('availOnly', 0);

            // 현재 테넌트의 바로빌 회원 정보 조회
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();

            // 테넌트별 서버 모드 적용
            if ($barobillMember && $barobillMember->server_mode) {
                $this->applyMemberServerMode($barobillMember);
            }

            // 바로빌 사용자 ID 결정
            $userId = $barobillMember?->barobill_id ?? '';

            $result = $this->callSoap('GetBankAccountEx', [
                'AvailOnly' => (int) $availOnly,
            ]);

            if (! $result['success']) {
                return response()->json([
                    'success' => false,
                    'error' => $result['error'],
                    'error_code' => $result['error_code'] ?? null,
                ]);
            }

            $accounts = [];
            $data = $result['data'];

            // BankAccount 또는 BankAccountEx에서 계좌 목록 추출
            $accountList = [];
            if (isset($data->BankAccount)) {
                $accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount];
            } elseif (isset($data->BankAccountEx)) {
                $accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx];
            }

            foreach ($accountList as $acc) {
                if (! is_object($acc)) {
                    continue;
                }

                $bankAccountNum = $acc->BankAccountNum ?? '';
                if (empty($bankAccountNum) || (is_numeric($bankAccountNum) && $bankAccountNum < 0)) {
                    continue;
                }

                $bankCode = $acc->BankCode ?? '';
                $bankName = $acc->BankName ?? $this->getBankName($bankCode);

                $accounts[] = [
                    'bankAccountNum' => $bankAccountNum,
                    'bankCode' => $bankCode,
                    'bankName' => $bankName,
                    'accountName' => $acc->AccountName ?? '',
                    'accountType' => $acc->AccountType ?? '',
                    'currency' => $acc->Currency ?? 'KRW',
                    'issueDate' => $acc->IssueDate ?? '',
                    'balance' => $acc->Balance ?? 0,
                    'status' => isset($acc->UseState) ? (int) $acc->UseState : 1,
                ];
            }

            return response()->json([
                'success' => true,
                'accounts' => $accounts,
                'count' => count($accounts),
            ]);
        } catch (\Throwable $e) {
            Log::error('계좌 목록 조회 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '서버 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 계좌별 최신 잔액 조회 (DB에서 조회 - 대시보드용)
     */
    public function latestBalances(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);

            // 전체 거래를 한번에 조회 (계좌별 → 시간순)
            $allTransactions = BankTransaction::where('tenant_id', $tenantId)
                ->select('bank_account_num', 'bank_name', 'balance', 'deposit', 'withdraw', 'trans_date', 'trans_time', 'is_manual')
                ->orderBy('bank_account_num')
                ->orderBy('trans_date', 'asc')
                ->orderBy('trans_time', 'asc')
                ->orderBy('id', 'asc')
                ->get();

            // 계좌별로 그룹화하여 최종 잔액 계산
            $accountBalances = [];
            foreach ($allTransactions as $tx) {
                $accNum = $tx->bank_account_num;

                if (! isset($accountBalances[$accNum])) {
                    $accountBalances[$accNum] = [
                        'bankAccountNum' => $accNum,
                        'bankName' => $tx->bank_name,
                        'balance' => 0,
                        'lastTransDate' => '',
                        'lastTransTime' => '',
                    ];
                }

                $prev = $accountBalances[$accNum]['balance'];
                if (! $tx->is_manual && (float) $tx->balance != 0) {
                    $accountBalances[$accNum]['balance'] = (float) $tx->balance;
                } else {
                    $accountBalances[$accNum]['balance'] = $prev + (float) $tx->deposit - (float) $tx->withdraw;
                }
                $accountBalances[$accNum]['lastTransDate'] = $tx->trans_date;
                $accountBalances[$accNum]['lastTransTime'] = $tx->trans_time;
            }

            $result = array_values($accountBalances);

            return response()->json([
                'success' => true,
                'balances' => $result,
                'count' => count($result),
            ]);
        } catch (\Throwable $e) {
            Log::error('최신 잔액 조회 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * 계좌 입출금내역 조회 (GetPeriodBankAccountTransLog)
     */
    public function transactions(Request $request): JsonResponse
    {
        try {
            $startDate = $request->input('startDate', date('Ymd'));
            $endDate = $request->input('endDate', date('Ymd'));
            $bankAccountNum = str_replace('-', '', $request->input('accountNum', ''));
            $page = (int) $request->input('page', 1);
            $limit = (int) $request->input('limit', 50);

            // 현재 테넌트의 바로빌 회원 정보 조회
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
            $userId = $barobillMember?->barobill_id ?? '';

            // 테넌트별 서버 모드 적용
            if ($barobillMember && $barobillMember->server_mode) {
                $this->applyMemberServerMode($barobillMember);
            }

            // DB에서 저장된 계정과목 데이터 조회
            $savedData = BankTransaction::getByDateRange($tenantId, $startDate, $endDate, $bankAccountNum ?: null);

            // 오버라이드 데이터 (수정된 적요/내용) 조회
            $overrideData = null;

            // DB에서 수동 입력 건 조회
            $manualQuery = BankTransaction::where('tenant_id', $tenantId)
                ->where('is_manual', true)
                ->whereBetween('trans_date', [$startDate, $endDate]);
            if (! empty($bankAccountNum)) {
                $manualQuery->where('bank_account_num', $bankAccountNum);
            }
            $manualTransactions = $manualQuery->orderBy('trans_date', 'desc')
                ->orderBy('trans_time', 'desc')
                ->get();

            // 전체 계좌 조회: 빈 값이면 모든 계좌의 거래 내역 조회
            if (empty($bankAccountNum)) {
                return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData, $overrideData, $tenantId, $manualTransactions);
            }

            // 단일 계좌 조회 - 기간을 월별로 분할하여 SOAP API 호출 (긴 기간 에러 방지)
            // 캐싱: tenantId 전달, bankName/bankCode는 단일 계좌에서는 빈 문자열
            $fetched = $this->fetchAccountTransactions($userId, $bankAccountNum, $startDate, $endDate, $tenantId, '', '');

            // API 데이터가 없는 경우 (수동 건만 표시)
            if (empty($fetched['logs'])) {
                $manualLogs = $this->convertManualToLogs($manualTransactions);
                $baseBalance = $this->findBaseBalance($tenantId, $startDate, $bankAccountNum);
                $recalcLogs = $this->recalcManualBalances($manualLogs['logs'], $baseBalance);

                return response()->json([
                    'success' => true,
                    'data' => [
                        'logs' => $recalcLogs,
                        'summary' => $manualLogs['summary'],
                        'pagination' => ['currentPage' => 1, 'maxPageNum' => 1],
                    ],
                ]);
            }

            // 월별 청크 결과를 합쳐서 파싱
            $fakeData = new \stdClass;
            $fakeData->BankAccountLogList = new \stdClass;
            $fakeData->BankAccountLogList->BankAccountTransLog = $fetched['logs'];

            $logs = $this->parseTransactionLogs($fakeData, '', $savedData, $tenantId);

            // 수동 입력 건 병합 (중복 제거: 수동 거래와 동일한 API 거래는 제외)
            $manualLogs = $this->convertManualToLogs($manualTransactions);
            $mergeResult = $this->mergeWithDedup($logs['logs'], $manualLogs['logs']);
            $mergedLogs = $mergeResult['logs'];

            // 날짜/시간 기준 정렬 (최신순)
            usort($mergedLogs, function ($a, $b) {
                $dtA = ($a['transDate'] ?? '').($a['transTime'] ?? '');
                $dtB = ($b['transDate'] ?? '').($b['transTime'] ?? '');

                return strcmp($dtB, $dtA);
            });

            // 수동입력 건 잔액 재계산 (조회기간 이전 잔액 기준)
            $baseBalance = $this->findBaseBalance($tenantId, $startDate, $bankAccountNum);
            $mergedLogs = $this->recalcManualBalances($mergedLogs, $baseBalance);

            // summary 합산 (중복 제거된 API 거래 금액 차감)
            $mergedSummary = [
                'totalDeposit' => $logs['summary']['totalDeposit'] + $manualLogs['summary']['totalDeposit'] - $mergeResult['removedDeposit'],
                'totalWithdraw' => $logs['summary']['totalWithdraw'] + $manualLogs['summary']['totalWithdraw'] - $mergeResult['removedWithdraw'],
                'count' => count($mergedLogs),
            ];

            // 클라이언트 사이드 페이지네이션
            $totalCount = count($mergedLogs);
            $maxPageNum = (int) ceil($totalCount / $limit);
            $startIndex = ($page - 1) * $limit;
            $paginatedLogs = array_slice($mergedLogs, $startIndex, $limit);

            return response()->json([
                'success' => true,
                'data' => [
                    'logs' => $paginatedLogs,
                    'pagination' => [
                        'currentPage' => $page,
                        'countPerPage' => $limit,
                        'maxPageNum' => $maxPageNum,
                        'maxIndex' => $totalCount,
                    ],
                    'summary' => $mergedSummary,
                ],
            ]);
        } catch (\Throwable $e) {
            Log::error('입출금내역 조회 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '서버 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 전체 계좌의 거래 내역 조회
     */
    private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null, $overrideData = null, int $tenantId = 1, $manualTransactions = null): JsonResponse
    {
        // 먼저 계좌 목록 조회
        $accountResult = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]);

        if (! $accountResult['success']) {
            return response()->json([
                'success' => false,
                'error' => $accountResult['error'],
            ]);
        }

        $accountList = [];
        $data = $accountResult['data'];
        if (isset($data->BankAccount)) {
            $accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount];
        } elseif (isset($data->BankAccountEx)) {
            $accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx];
        }

        $allLogs = [];
        $totalDeposit = 0;
        $totalWithdraw = 0;

        foreach ($accountList as $acc) {
            if (! is_object($acc)) {
                continue;
            }

            $accNum = $acc->BankAccountNum ?? '';
            if (empty($accNum) || (is_numeric($accNum) && $accNum < 0)) {
                continue;
            }

            // 기간을 월별로 분할하여 SOAP API 호출 (긴 기간 에러 방지)
            // 캐싱: 계좌별 bankName/bankCode 전달
            $fetched = $this->fetchAccountTransactions($userId, $accNum, $startDate, $endDate, $tenantId, $acc->BankName ?? '', $acc->BankCode ?? '');

            if (! empty($fetched['logs'])) {
                $fakeData = new \stdClass;
                $fakeData->BankAccountLogList = new \stdClass;
                $fakeData->BankAccountLogList->BankAccountTransLog = $fetched['logs'];

                $parsed = $this->parseTransactionLogs($fakeData, $acc->BankName ?? '', $savedData, $tenantId);
                foreach ($parsed['logs'] as $log) {
                    $log['bankName'] = $acc->BankName ?? $this->getBankName($acc->BankCode ?? '');
                    $allLogs[] = $log;
                }
                $totalDeposit += $parsed['summary']['totalDeposit'];
                $totalWithdraw += $parsed['summary']['totalWithdraw'];
            }
        }

        // 수동 입력 건 병합 (중복 제거: 수동 거래와 동일한 API 거래는 제외)
        if ($manualTransactions && $manualTransactions->isNotEmpty()) {
            $manualLogs = $this->convertManualToLogs($manualTransactions);
            $mergeResult = $this->mergeWithDedup($allLogs, $manualLogs['logs']);
            $allLogs = $mergeResult['logs'];
            $totalDeposit += $manualLogs['summary']['totalDeposit'] - $mergeResult['removedDeposit'];
            $totalWithdraw += $manualLogs['summary']['totalWithdraw'] - $mergeResult['removedWithdraw'];
        }

        // 날짜/시간 기준 정렬 (최신순)
        usort($allLogs, function ($a, $b) {
            $dateA = ($a['transDate'] ?? '').($a['transTime'] ?? '');
            $dateB = ($b['transDate'] ?? '').($b['transTime'] ?? '');

            return strcmp($dateB, $dateA);
        });

        // 수동입력 건 잔액 재계산 (조회기간 이전 잔액 기준)
        $baseBalance = $this->findBaseBalance($tenantId, $startDate);
        $allLogs = $this->recalcManualBalances($allLogs, $baseBalance);

        // 페이지네이션
        $totalCount = count($allLogs);
        $maxPageNum = (int) ceil($totalCount / $limit);
        $startIndex = ($page - 1) * $limit;
        $paginatedLogs = array_slice($allLogs, $startIndex, $limit);

        return response()->json([
            'success' => true,
            'data' => [
                'logs' => $paginatedLogs,
                'pagination' => [
                    'currentPage' => $page,
                    'countPerPage' => $limit,
                    'maxPageNum' => $maxPageNum,
                    'maxIndex' => $totalCount,
                ],
                'summary' => [
                    'totalDeposit' => $totalDeposit,
                    'totalWithdraw' => $totalWithdraw,
                    'count' => $totalCount,
                ],
            ],
        ]);
    }

    /**
     * 거래 내역 파싱 (저장된 계정과목 + 오버라이드 병합)
     */
    private function parseTransactionLogs($resultData, string $defaultBankName = '', $savedData = null, int $tenantId = 1): array
    {
        $logs = [];
        $uniqueKeys = [];
        $totalDeposit = 0;
        $totalWithdraw = 0;

        $rawLogs = [];
        if (isset($resultData->BankAccountLogList) && isset($resultData->BankAccountLogList->BankAccountTransLog)) {
            $rawLogs = is_array($resultData->BankAccountLogList->BankAccountTransLog)
                ? $resultData->BankAccountLogList->BankAccountTransLog
                : [$resultData->BankAccountLogList->BankAccountTransLog];
        }

        // 1단계: 모든 고유 키 수집
        foreach ($rawLogs as $log) {
            $bankAccountNum = $log->BankAccountNum ?? '';
            $transDT = $log->TransDT ?? '';
            $deposit = (int) floatval($log->Deposit ?? 0);
            $withdraw = (int) floatval($log->Withdraw ?? 0);
            $balance = (int) floatval($log->Balance ?? 0);
            $summary = $log->TransRemark1 ?? $log->Summary ?? '';

            $uniqueKey = implode('|', [$bankAccountNum, $transDT, $deposit, $withdraw, $balance, $summary]);
            $uniqueKeys[] = $uniqueKey;
        }

        // 2단계: 오버라이드 데이터 일괄 조회
        $overrides = BankTransactionOverride::getByUniqueKeys($tenantId, $uniqueKeys);

        // 3단계: 각 로그 처리
        foreach ($rawLogs as $log) {
            $deposit = floatval($log->Deposit ?? 0);
            $withdraw = floatval($log->Withdraw ?? 0);
            $balance = floatval($log->Balance ?? 0);
            $totalDeposit += $deposit;
            $totalWithdraw += $withdraw;

            // 거래일시 파싱
            $transDT = $log->TransDT ?? '';
            $transDate = '';
            $transTime = '';
            $dateTime = '';

            if (! empty($transDT) && strlen($transDT) >= 14) {
                $transDate = substr($transDT, 0, 8);
                $transTime = substr($transDT, 8, 6);
                $dateTime = substr($transDT, 0, 4).'-'.substr($transDT, 4, 2).'-'.substr($transDT, 6, 2).' '.
                    substr($transDT, 8, 2).':'.substr($transDT, 10, 2).':'.substr($transDT, 12, 2);
            }

            // 적요 파싱 (TransRemark1만 적요로, TransRemark2는 상대계좌예금주명으로 분리)
            $summary = $log->TransRemark1 ?? $log->Summary ?? '';
            $remark2 = $log->TransRemark2 ?? '';
            $transType = $log->TransType ?? '';

            $bankAccountNum = $log->BankAccountNum ?? '';

            // 고유 키 생성하여 저장된 데이터와 매칭 (숫자는 정수로 변환하여 형식 통일)
            $uniqueKey = implode('|', [$bankAccountNum, $transDT, (int) $deposit, (int) $withdraw, (int) $balance, $summary]);
            $savedItem = $savedData?->get($uniqueKey);
            $override = $overrides->get($uniqueKey);

            // 원본 적요/내용 (remark2를 합산하지 않음 - 상대계좌예금주명 컬럼에서 별도 표시)
            $originalSummary = $summary;
            $originalCast = $savedItem?->cast ?? $remark2;

            // 오버라이드 적용 (수정된 값이 있으면 사용)
            $displaySummary = $override?->modified_summary ?? $originalSummary;
            $displayCast = $override?->modified_cast ?? $originalCast;

            $logItem = [
                'transDate' => $transDate,
                'transTime' => $transTime,
                'transDateTime' => $dateTime,
                'bankAccountNum' => $bankAccountNum,
                'bankName' => $log->BankName ?? $defaultBankName,
                'deposit' => $deposit,
                'withdraw' => $withdraw,
                'depositFormatted' => number_format($deposit),
                'withdrawFormatted' => number_format($withdraw),
                'balance' => $balance,
                'balanceFormatted' => number_format($balance),
                'summary' => $displaySummary,
                'originalSummary' => $originalSummary,
                'cast' => $displayCast,
                'originalCast' => $originalCast,
                'memo' => $log->Memo ?? '',
                'transOffice' => $log->TransOffice ?? '',
                // 저장된 계정과목 정보 병합
                'accountCode' => $savedItem?->account_code ?? '',
                'accountName' => $savedItem?->account_name ?? '',
                // 저장된 거래처 정보 병합
                'clientCode' => $savedItem?->client_code ?? '',
                'clientName' => $savedItem?->client_name ?? '',
                'isSaved' => $savedItem !== null,
                'isOverridden' => $override !== null,
                'uniqueKey' => $uniqueKey,
            ];

            $logs[] = $logItem;
        }

        return [
            'logs' => $logs,
            'summary' => [
                'totalDeposit' => $totalDeposit,
                'totalWithdraw' => $totalWithdraw,
                'count' => count($logs),
            ],
        ];
    }

    /**
     * 에러 코드 체크
     */
    private function checkErrorCode($data): ?int
    {
        if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) {
            return (int) $data->CurrentPage;
        }
        if (isset($data->BankAccountNum) && is_numeric($data->BankAccountNum) && $data->BankAccountNum < 0) {
            return (int) $data->BankAccountNum;
        }

        return null;
    }

    /**
     * 에러 메시지 반환
     */
    private function getErrorMessage(int $errorCode): string
    {
        $messages = [
            -10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다.',
            -50214 => '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.',
            -24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.',
            -25001 => '등록된 계좌가 없습니다 (-25001).',
            -25005 => '조회된 데이터가 없습니다 (-25005).',
            -25006 => '계좌번호가 잘못되었습니다 (-25006).',
            -25007 => '조회 기간이 잘못되었습니다 (-25007).',
        ];

        return $messages[$errorCode] ?? '바로빌 API 오류: '.$errorCode;
    }

    /**
     * 긴 기간을 월별 청크로 분할 (바로빌 API 기간 제한 대응)
     * YYYYMMDD 형식의 시작/종료일을 받아 월별 [start, end] 배열 반환
     */
    private function splitDateRangeMonthly(string $startDate, string $endDate): array
    {
        $start = Carbon::createFromFormat('Ymd', $startDate)->startOfDay();
        $end = Carbon::createFromFormat('Ymd', $endDate)->endOfDay();

        $chunks = [];
        $cursor = $start->copy();

        while ($cursor->lte($end)) {
            $chunkStart = $cursor->copy();
            $chunkEnd = $cursor->copy()->endOfMonth()->startOfDay();

            // 마지막 청크: 종료일이 월말보다 이전이면 종료일 사용
            if ($chunkEnd->gt($end)) {
                $chunkEnd = $end->copy()->startOfDay();
            }

            $chunks[] = [
                'start' => $chunkStart->format('Ymd'),
                'end' => $chunkEnd->format('Ymd'),
            ];

            // 다음 월 1일로 이동
            $cursor = $chunkEnd->copy()->addDay()->startOfMonth();
        }

        return $chunks;
    }

    /**
     * 단일 계좌의 거래 내역을 기간 분할하여 조회 (캐싱 지원)
     * 긴 기간도 월별로 나누어 캐시/SOAP API 호출 후 병합
     *
     * 캐시 판단 로직:
     * - 과거 월 (현재 월 이전): sync 레코드 존재 → DB에서 제공 (API 호출 안 함)
     * - 현재 월: sync 레코드 존재 + synced_at이 10분 이내 → DB에서 제공
     * - 미동기화: API 호출 → DB 저장 → sync 레코드 생성/갱신
     */
    private function fetchAccountTransactions(string $userId, string $accNum, string $startDate, string $endDate, int $tenantId = 0, string $bankName = '', string $bankCode = ''): array
    {
        $chunks = $this->splitDateRangeMonthly($startDate, $endDate);
        $allRawLogs = [];
        $lastSuccessData = null;
        $currentYearMonth = Carbon::now()->format('Ym');

        foreach ($chunks as $chunk) {
            $yearMonth = substr($chunk['start'], 0, 6);
            $isCurrentMonth = ($yearMonth === $currentYearMonth);

            // 캐시 판단 (tenantId가 전달된 경우에만)
            $useCache = false;
            if ($tenantId > 0) {
                $syncStatus = BankSyncStatus::where('tenant_id', $tenantId)
                    ->where('bank_account_num', $accNum)
                    ->where('synced_year_month', $yearMonth)
                    ->first();

                if ($syncStatus) {
                    if (! $isCurrentMonth) {
                        $useCache = true; // 과거 월: 항상 캐시
                    } elseif ($syncStatus->synced_at->diffInMinutes(now()) < 10) {
                        $useCache = true; // 현재 월: 10분 이내면 캐시
                    }
                }
            }

            if ($useCache) {
                // DB에서 캐시된 거래 조회 → stdClass 변환
                $cachedRecords = BankTransaction::getCachedByMonth($tenantId, $accNum, $chunk['start'], $chunk['end']);
                foreach ($cachedRecords as $record) {
                    $allRawLogs[] = $this->convertDbToRawLog($record);
                }
                Log::debug("바로빌 캐시 사용 ({$chunk['start']}~{$chunk['end']}): {$cachedRecords->count()}건");

                continue;
            }

            // API 호출
            $result = $this->callSoap('GetPeriodBankAccountTransLog', [
                'ID' => $userId,
                'BankAccountNum' => $accNum,
                'StartDate' => $chunk['start'],
                'EndDate' => $chunk['end'],
                'TransDirection' => 1,
                'CountPerPage' => 1000,
                'CurrentPage' => 1,
                'OrderDirection' => 2,
            ]);

            if (! $result['success']) {
                continue;
            }

            $chunkData = $result['data'];
            $errorCode = $this->checkErrorCode($chunkData);

            // 데이터 없음(-25005, -25001)은 건너뜀, 기타 에러도 건너뜀 (다른 월은 성공할 수 있음)
            if ($errorCode && ! in_array($errorCode, [-25005, -25001])) {
                Log::debug("바로빌 API 기간 분할 - 에러 발생 ({$chunk['start']}~{$chunk['end']}): {$errorCode}");

                continue;
            }

            if ($errorCode && in_array($errorCode, [-25005, -25001])) {
                // 데이터 없음이어도 sync 상태 기록 (빈 월 반복 호출 방지)
                if ($tenantId > 0) {
                    $this->updateSyncStatus($tenantId, $accNum, $yearMonth);
                }

                continue;
            }

            // 로그 추출
            $rawLogs = [];
            if (isset($chunkData->BankAccountLogList) && isset($chunkData->BankAccountLogList->BankAccountTransLog)) {
                $logs = $chunkData->BankAccountLogList->BankAccountTransLog;
                $rawLogs = is_array($logs) ? $logs : [$logs];
            }

            foreach ($rawLogs as $log) {
                $allRawLogs[] = $log;
            }

            // 캐시 저장 (tenantId가 전달된 경우에만)
            if ($tenantId > 0 && ! empty($rawLogs)) {
                $this->cacheApiTransactions($tenantId, $accNum, $bankName, $bankCode, $rawLogs);
                $this->updateSyncStatus($tenantId, $accNum, $yearMonth);
                Log::debug("바로빌 API 캐시 저장 ({$chunk['start']}~{$chunk['end']}): ".count($rawLogs).'건');
            } elseif ($tenantId > 0) {
                $this->updateSyncStatus($tenantId, $accNum, $yearMonth);
            }

            $lastSuccessData = $chunkData;
        }

        return [
            'logs' => $allRawLogs,
            'lastData' => $lastSuccessData,
        ];
    }

    /**
     * SOAP API 결과를 DB에 배치 저장 (캐시)
     * insertOrIgnore 사용으로 기존 레코드(사용자가 account_code 할당한 것) 보호
     */
    private function cacheApiTransactions(int $tenantId, string $accNum, string $bankName, string $bankCode, array $rawLogs): void
    {
        $rows = [];
        $now = now();

        foreach ($rawLogs as $log) {
            $transDT = $log->TransDT ?? '';
            $transDate = strlen($transDT) >= 8 ? substr($transDT, 0, 8) : '';
            $transTime = strlen($transDT) >= 14 ? substr($transDT, 8, 6) : '';
            $deposit = floatval($log->Deposit ?? 0);
            $withdraw = floatval($log->Withdraw ?? 0);
            $balance = floatval($log->Balance ?? 0);
            $summary = $log->TransRemark1 ?? $log->Summary ?? '';
            $remark2 = $log->TransRemark2 ?? '';

            $rows[] = [
                'tenant_id' => $tenantId,
                'bank_account_num' => $log->BankAccountNum ?? $accNum,
                'bank_code' => $log->BankCode ?? $bankCode,
                'bank_name' => $log->BankName ?? $bankName,
                'trans_date' => $transDate,
                'trans_time' => $transTime,
                'trans_dt' => $transDT,
                'deposit' => $deposit,
                'withdraw' => $withdraw,
                'balance' => $balance,
                'summary' => $summary,
                'cast' => $remark2,
                'memo' => $log->Memo ?? '',
                'trans_office' => $log->TransOffice ?? '',
                'is_manual' => false,
                'created_at' => $now,
                'updated_at' => $now,
            ];
        }

        // 100건씩 배치 insertOrIgnore (unique constraint로 중복 방지)
        foreach (array_chunk($rows, 100) as $batch) {
            DB::table('barobill_bank_transactions')->insertOrIgnore($batch);
        }
    }

    /**
     * DB 레코드를 SOAP BankAccountTransLog 객체 형태로 변환
     * parseTransactionLogs()에 그대로 전달 가능
     */
    private function convertDbToRawLog(BankTransaction $record): \stdClass
    {
        $log = new \stdClass;
        $log->BankAccountNum = $record->bank_account_num;
        $log->TransDT = $record->trans_dt;
        $log->Deposit = $record->deposit;
        $log->Withdraw = $record->withdraw;
        $log->Balance = $record->balance;
        $log->TransRemark1 = $record->summary ?? '';
        $log->TransRemark2 = $record->cast ?? '';
        $log->Memo = $record->memo ?? '';
        $log->TransOffice = $record->trans_office ?? '';
        $log->TransType = '';
        $log->BankName = $record->bank_name ?? '';
        $log->BankCode = $record->bank_code ?? '';
        $log->ClientCode = $record->client_code ?? '';
        $log->ClientName = $record->client_name ?? '';

        return $log;
    }

    /**
     * 동기화 상태 갱신
     */
    private function updateSyncStatus(int $tenantId, string $accNum, string $yearMonth): void
    {
        BankSyncStatus::updateOrCreate(
            [
                'tenant_id' => $tenantId,
                'bank_account_num' => $accNum,
                'synced_year_month' => $yearMonth,
            ],
            [
                'synced_at' => now(),
            ]
        );
    }

    /**
     * 은행 코드 -> 은행명 변환
     */
    private function getBankName(string $code): string
    {
        $banks = [
            '002' => 'KDB산업은행',
            '003' => 'IBK기업은행',
            '004' => 'KB국민은행',
            '007' => '수협은행',
            '011' => 'NH농협은행',
            '020' => '우리은행',
            '023' => 'SC제일은행',
            '027' => '한국씨티은행',
            '031' => '대구은행',
            '032' => '부산은행',
            '034' => '광주은행',
            '035' => '제주은행',
            '037' => '전북은행',
            '039' => '경남은행',
            '045' => '새마을금고',
            '048' => '신협',
            '050' => '저축은행',
            '064' => '산림조합',
            '071' => '우체국',
            '081' => '하나은행',
            '088' => '신한은행',
            '089' => 'K뱅크',
            '090' => '카카오뱅크',
            '092' => '토스뱅크',
        ];

        return $banks[$code] ?? $code;
    }

    /**
     * 거래처 검색 API
     */
    public function searchClients(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $keyword = trim($request->input('q', ''));

            if (empty($keyword)) {
                return response()->json([
                    'success' => true,
                    'data' => [],
                ]);
            }

            $query = TradingPartner::forTenant($tenantId)
                ->where('status', 'active');

            if (is_numeric($keyword)) {
                $query->where(function ($q) use ($keyword) {
                    $q->where('id', $keyword)
                        ->orWhere('name', 'like', "%{$keyword}%");
                });
            } else {
                $query->where('name', 'like', "%{$keyword}%");
            }

            $clients = $query->select('id', 'name')
                ->orderBy('id')
                ->limit(20)
                ->get();

            return response()->json([
                'success' => true,
                'data' => $clients->map(fn ($c) => [
                    'code' => (string) $c->id,
                    'name' => $c->name,
                ]),
            ]);
        } catch (\Throwable $e) {
            Log::error('거래처 검색 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '검색 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 계정과목 목록 조회 (글로벌 데이터)
     */
    public function accountCodes(): JsonResponse
    {
        $codes = AccountCode::getActive();

        return response()->json([
            'success' => true,
            'data' => $codes->map(fn ($c) => [
                'id' => $c->id,
                'code' => $c->code,
                'name' => $c->name,
                'category' => $c->category,
            ]),
        ]);
    }

    /**
     * 전체 계정과목 목록 조회 (설정용, 비활성 포함, 글로벌 데이터)
     */
    public function accountCodesAll(): JsonResponse
    {
        $codes = AccountCode::getAll();

        return response()->json([
            'success' => true,
            'data' => $codes,
        ]);
    }

    /**
     * 계정과목 추가 (글로벌 데이터)
     */
    public function accountCodeStore(Request $request): JsonResponse
    {
        try {
            $validated = $request->validate([
                'code' => 'required|string|max:10',
                'name' => 'required|string|max:100',
                'category' => 'nullable|string|max:50',
            ]);

            // 중복 체크 (글로벌)
            $exists = AccountCode::where('code', $validated['code'])->exists();

            if ($exists) {
                return response()->json([
                    'success' => false,
                    'error' => '이미 존재하는 계정과목 코드입니다.',
                ], 422);
            }

            $maxSort = AccountCode::max('sort_order') ?? 0;

            $accountCode = AccountCode::create([
                'tenant_id' => self::HEADQUARTERS_TENANT_ID, // 글로벌 데이터는 기본 테넌트에 저장
                'code' => $validated['code'],
                'name' => $validated['name'],
                'category' => $validated['category'] ?? null,
                'sort_order' => $maxSort + 1,
                'is_active' => true,
            ]);

            return response()->json([
                'success' => true,
                'message' => '계정과목이 추가되었습니다.',
                'data' => $accountCode,
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'error' => '추가 실패: '.$e->getMessage(),
            ], 500);
        }
    }

    /**
     * 계정과목 수정 (글로벌 데이터)
     */
    public function accountCodeUpdate(Request $request, int $id): JsonResponse
    {
        try {
            $accountCode = AccountCode::find($id);

            if (! $accountCode) {
                return response()->json([
                    'success' => false,
                    'error' => '계정과목을 찾을 수 없습니다.',
                ], 404);
            }

            $validated = $request->validate([
                'code' => 'sometimes|string|max:10',
                'name' => 'sometimes|string|max:100',
                'category' => 'nullable|string|max:50',
                'is_active' => 'sometimes|boolean',
            ]);

            // 코드 변경 시 중복 체크 (글로벌)
            if (isset($validated['code']) && $validated['code'] !== $accountCode->code) {
                $exists = AccountCode::where('code', $validated['code'])
                    ->where('id', '!=', $id)
                    ->exists();

                if ($exists) {
                    return response()->json([
                        'success' => false,
                        'error' => '이미 존재하는 계정과목 코드입니다.',
                    ], 422);
                }
            }

            $accountCode->update($validated);

            return response()->json([
                'success' => true,
                'message' => '계정과목이 수정되었습니다.',
                'data' => $accountCode,
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'error' => '수정 실패: '.$e->getMessage(),
            ], 500);
        }
    }

    /**
     * 계정과목 삭제 (글로벌 데이터)
     */
    public function accountCodeDestroy(int $id): JsonResponse
    {
        try {
            $accountCode = AccountCode::find($id);

            if (! $accountCode) {
                return response()->json([
                    'success' => false,
                    'error' => '계정과목을 찾을 수 없습니다.',
                ], 404);
            }

            $accountCode->delete();

            return response()->json([
                'success' => true,
                'message' => '계정과목이 삭제되었습니다.',
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'error' => '삭제 실패: '.$e->getMessage(),
            ], 500);
        }
    }

    /**
     * 입출금 내역 저장 (계정과목 포함)
     */
    public function save(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $transactions = $request->input('transactions', []);

            Log::info('[Eaccount Save] 요청 수신', [
                'tenant_id' => $tenantId,
                'transaction_count' => count($transactions),
            ]);

            if (empty($transactions)) {
                return response()->json([
                    'success' => false,
                    'error' => '저장할 데이터가 없습니다.',
                ]);
            }

            // 수동 거래 디버그
            $manualCount = 0;
            $apiCount = 0;
            foreach ($transactions as $t) {
                if (! empty($t['isManual'])) {
                    $manualCount++;
                    Log::info('[Eaccount Save] 수동 거래 감지', [
                        'dbId' => $t['dbId'] ?? 'MISSING',
                        'cast' => $t['cast'] ?? 'EMPTY',
                        'isManual' => $t['isManual'],
                    ]);
                } else {
                    $apiCount++;
                }
            }
            Log::info('[Eaccount Save] 거래 분류', ['manual' => $manualCount, 'api' => $apiCount]);

            $saved = 0;
            $updated = 0;
            $savedUniqueKeys = [];

            DB::beginTransaction();

            foreach ($transactions as $trans) {
                // 수동 입력 거래: dbId로 직접 찾아서 비-키 필드만 업데이트
                // balance는 화면에서 재계산된 값이므로 composite key 매칭 불가
                if (! empty($trans['isManual']) && ! empty($trans['dbId'])) {
                    $affectedRows = DB::table('barobill_bank_transactions')
                        ->where('id', $trans['dbId'])
                        ->where('tenant_id', $tenantId)
                        ->update([
                            'summary' => $trans['summary'] ?? '',
                            'cast' => $trans['cast'] ?? '',
                            'memo' => $trans['memo'] ?? '',
                            'trans_office' => $trans['transOffice'] ?? '',
                            'account_code' => $trans['accountCode'] ?? null,
                            'account_name' => $trans['accountName'] ?? null,
                            'client_code' => $trans['clientCode'] ?? null,
                            'client_name' => $trans['clientName'] ?? null,
                            'updated_at' => now(),
                        ]);
                    Log::info('[Eaccount Save] 수동 거래 업데이트', [
                        'dbId' => $trans['dbId'],
                        'cast' => $trans['cast'] ?? '',
                        'affected_rows' => $affectedRows,
                    ]);
                    $updated++;

                    continue;
                }

                // 거래일시 생성
                $transDt = ($trans['transDate'] ?? '').($trans['transTime'] ?? '');

                $data = [
                    'tenant_id' => $tenantId,
                    'bank_account_num' => $trans['bankAccountNum'] ?? '',
                    'bank_code' => $trans['bankCode'] ?? '',
                    'bank_name' => $trans['bankName'] ?? '',
                    'trans_date' => $trans['transDate'] ?? '',
                    'trans_time' => $trans['transTime'] ?? '',
                    'trans_dt' => $transDt,
                    'deposit' => floatval($trans['deposit'] ?? 0),
                    'withdraw' => floatval($trans['withdraw'] ?? 0),
                    'balance' => floatval($trans['balance'] ?? 0),
                    'summary' => $trans['summary'] ?? '',
                    'cast' => $trans['cast'] ?? '',
                    'memo' => $trans['memo'] ?? '',
                    'trans_office' => $trans['transOffice'] ?? '',
                    'account_code' => $trans['accountCode'] ?? null,
                    'account_name' => $trans['accountName'] ?? null,
                    'client_code' => $trans['clientCode'] ?? null,
                    'client_name' => $trans['clientName'] ?? null,
                ];

                // 고유 키 생성 (오버라이드 동기화용)
                $uniqueKey = implode('|', [
                    $data['bank_account_num'],
                    $transDt,
                    (int) $data['deposit'],
                    (int) $data['withdraw'],
                    (int) $data['balance'],
                    $data['summary'],
                ]);
                $savedUniqueKeys[] = $uniqueKey;

                // 순수 Query Builder로 Upsert (Eloquent 모델 우회)
                // balance + summary 포함하여 매칭 → 같은 금액·시간이라도 적요가 다르면 별도 거래로 식별
                $existingIds = DB::table('barobill_bank_transactions')
                    ->where('tenant_id', $tenantId)
                    ->where('bank_account_num', $data['bank_account_num'])
                    ->where('trans_dt', $transDt)
                    ->where('deposit', $data['deposit'])
                    ->where('withdraw', $data['withdraw'])
                    ->where('balance', $data['balance'])
                    ->where('summary', $data['summary'])
                    ->orderByDesc('id')
                    ->pluck('id');

                if ($existingIds->isNotEmpty()) {
                    $keepId = $existingIds->first(); // 최신 건 유지

                    // 완전 동일한 중복 건 삭제
                    if ($existingIds->count() > 1) {
                        DB::table('barobill_bank_transactions')
                            ->whereIn('id', $existingIds->slice(1)->values())
                            ->delete();
                    }

                    DB::table('barobill_bank_transactions')
                        ->where('id', $keepId)
                        ->update([
                            'summary' => $data['summary'],
                            'cast' => $data['cast'],
                            'trans_office' => $data['trans_office'],
                            'account_code' => $data['account_code'],
                            'account_name' => $data['account_name'],
                            'client_code' => $data['client_code'],
                            'client_name' => $data['client_name'],
                            'updated_at' => now(),
                        ]);
                    $updated++;
                } else {
                    DB::table('barobill_bank_transactions')->insert(array_merge($data, [
                        'created_at' => now(),
                        'updated_at' => now(),
                    ]));
                    $saved++;
                }
            }

            // 오버라이드 동기화: 메인 테이블에 저장된 값이 최신이므로
            // override의 modified_cast를 제거하여 충돌 방지
            if (! empty($savedUniqueKeys)) {
                $overrides = BankTransactionOverride::forTenant($tenantId)
                    ->whereIn('unique_key', $savedUniqueKeys)
                    ->get();

                foreach ($overrides as $override) {
                    if ($override->modified_cast !== null) {
                        if (! empty($override->modified_summary)) {
                            // summary 오버라이드는 유지, cast 오버라이드만 제거
                            $override->update(['modified_cast' => null]);
                        } else {
                            // summary도 없으면 오버라이드 레코드 삭제
                            $override->delete();
                        }
                    }
                }
            }

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => "저장 완료: 신규 {$saved}건, 수정 {$updated}건",
                'saved' => $saved,
                'updated' => $updated,
            ]);
        } catch (\Throwable $e) {
            DB::rollBack();
            Log::error('입출금 내역 저장 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '저장 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 엑셀 다운로드
     */
    public function exportExcel(Request $request): StreamedResponse|JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $startDate = $request->input('startDate', date('Ymd'));
            $endDate = $request->input('endDate', date('Ymd'));
            $accountNum = $request->input('accountNum', '');

            // DB에서 저장된 데이터 조회
            $query = BankTransaction::where('tenant_id', $tenantId)
                ->whereBetween('trans_date', [$startDate, $endDate])
                ->orderBy('trans_date', 'desc')
                ->orderBy('trans_time', 'desc');

            if (! empty($accountNum)) {
                $query->where('bank_account_num', $accountNum);
            }

            $transactions = $query->get();

            // 데이터가 없으면 바로빌에서 조회 (저장 안된 경우)
            if ($transactions->isEmpty()) {
                return response()->json([
                    'success' => false,
                    'error' => '저장된 데이터가 없습니다. 먼저 데이터를 조회하고 저장해주세요.',
                ]);
            }

            $filename = "입출금내역_{$startDate}_{$endDate}.csv";

            return response()->streamDownload(function () use ($transactions) {
                $handle = fopen('php://output', 'w');

                // UTF-8 BOM for Excel
                fprintf($handle, chr(0xEF).chr(0xBB).chr(0xBF));

                // 헤더
                fputcsv($handle, [
                    '거래일시',
                    '은행명',
                    '계좌번호',
                    '적요',
                    '입금',
                    '출금',
                    '잔액',
                    '취급점',
                    '상대계좌예금주명',
                    '계정과목코드',
                    '계정과목명',
                ]);

                // 데이터
                foreach ($transactions as $trans) {
                    $dateTime = '';
                    if ($trans->trans_date) {
                        $dateTime = substr($trans->trans_date, 0, 4).'-'.
                            substr($trans->trans_date, 4, 2).'-'.
                            substr($trans->trans_date, 6, 2);
                        if ($trans->trans_time) {
                            $dateTime .= ' '.substr($trans->trans_time, 0, 2).':'.
                                substr($trans->trans_time, 2, 2).':'.
                                substr($trans->trans_time, 4, 2);
                        }
                    }

                    fputcsv($handle, [
                        $dateTime,
                        $trans->bank_name,
                        $trans->bank_account_num,
                        $trans->summary,
                        $trans->deposit,
                        $trans->withdraw,
                        $trans->balance,
                        $trans->trans_office,
                        $trans->cast,
                        $trans->account_code,
                        $trans->account_name,
                    ]);
                }

                fclose($handle);
            }, $filename, [
                'Content-Type' => 'text/csv; charset=utf-8',
                'Content-Disposition' => 'attachment; filename="'.$filename.'"',
            ]);
        } catch (\Throwable $e) {
            Log::error('엑셀 다운로드 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '다운로드 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 거래내역 적요/내용 오버라이드 저장
     */
    public function saveOverride(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);

            $validated = $request->validate([
                'uniqueKey' => 'required|string|max:100',
                'modifiedSummary' => 'nullable|string|max:200',
                'modifiedCast' => 'nullable|string|max:200',
            ]);

            $result = BankTransactionOverride::saveOverride(
                $tenantId,
                $validated['uniqueKey'],
                $validated['modifiedSummary'] ?? null,
                $validated['modifiedCast'] ?? null
            );

            if ($result === null) {
                return response()->json([
                    'success' => true,
                    'message' => '오버라이드가 삭제되었습니다.',
                    'deleted' => true,
                ]);
            }

            return response()->json([
                'success' => true,
                'message' => '오버라이드가 저장되었습니다.',
                'data' => [
                    'id' => $result->id,
                    'modifiedSummary' => $result->modified_summary,
                    'modifiedCast' => $result->modified_cast,
                ],
            ]);
        } catch (\Throwable $e) {
            Log::error('오버라이드 저장 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '저장 오류: '.$e->getMessage(),
            ], 500);
        }
    }

    /**
     * 수동 거래 등록
     */
    public function storeManual(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);

            $validated = $request->validate([
                'bank_account_num' => 'required|string',
                'bank_code' => 'nullable|string',
                'bank_name' => 'nullable|string',
                'trans_date' => 'required|date_format:Ymd',
                'trans_time' => 'nullable|string|max:6',
                'deposit' => 'required|numeric',
                'withdraw' => 'required|numeric',
                'balance' => 'nullable|numeric',
                'summary' => 'nullable|string',
                'cast' => 'nullable|string',
                'memo' => 'nullable|string',
                'trans_office' => 'nullable|string',
                'account_code' => 'nullable|string',
                'account_name' => 'nullable|string',
            ]);

            $transTime = $validated['trans_time'] ?? '000000';
            $transDt = $validated['trans_date'].$transTime;

            $transaction = BankTransaction::create([
                'tenant_id' => $tenantId,
                'bank_account_num' => $validated['bank_account_num'],
                'bank_code' => $validated['bank_code'] ?? '',
                'bank_name' => $validated['bank_name'] ?? '',
                'trans_date' => $validated['trans_date'],
                'trans_time' => $transTime,
                'trans_dt' => $transDt,
                'deposit' => $validated['deposit'],
                'withdraw' => $validated['withdraw'],
                'balance' => $validated['balance'] ?? 0,
                'summary' => $validated['summary'] ?? '',
                'cast' => $validated['cast'] ?? '',
                'memo' => $validated['memo'] ?? '',
                'trans_office' => $validated['trans_office'] ?? '',
                'account_code' => $validated['account_code'] ?? null,
                'account_name' => $validated['account_name'] ?? null,
                'is_manual' => true,
            ]);

            return response()->json([
                'success' => true,
                'message' => '수동 거래가 등록되었습니다.',
                'data' => ['id' => $transaction->id],
            ]);
        } catch (\Illuminate\Validation\ValidationException $e) {
            return response()->json([
                'success' => false,
                'error' => '입력 데이터가 올바르지 않습니다.',
                'errors' => $e->errors(),
            ], 422);
        } catch (\Throwable $e) {
            Log::error('수동 거래 등록 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '등록 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 수동 거래 수정
     */
    public function updateManual(Request $request, int $id): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);

            $transaction = BankTransaction::where('id', $id)
                ->where('tenant_id', $tenantId)
                ->where('is_manual', true)
                ->firstOrFail();

            $validated = $request->validate([
                'bank_account_num' => 'required|string',
                'bank_code' => 'nullable|string',
                'bank_name' => 'nullable|string',
                'trans_date' => 'required|date_format:Ymd',
                'trans_time' => 'nullable|string|max:6',
                'deposit' => 'required|numeric',
                'withdraw' => 'required|numeric',
                'balance' => 'nullable|numeric',
                'summary' => 'nullable|string',
                'cast' => 'nullable|string',
                'memo' => 'nullable|string',
                'trans_office' => 'nullable|string',
                'account_code' => 'nullable|string',
                'account_name' => 'nullable|string',
            ]);

            $transTime = $validated['trans_time'] ?? '000000';
            $transDt = $validated['trans_date'].$transTime;

            // 수동 거래 수정: unique key 컬럼(deposit/withdraw/balance)은 제외
            // balance는 화면에서 재계산(recalcManualBalances)되므로 DB값 유지 필수
            // (프론트에서 재계산된 balance를 보내면 다른 레코드와 unique key 충돌)
            DB::table('barobill_bank_transactions')
                ->where('id', $transaction->id)
                ->update([
                    'summary' => $validated['summary'] ?? '',
                    'cast' => $validated['cast'] ?? '',
                    'memo' => $validated['memo'] ?? '',
                    'trans_office' => $validated['trans_office'] ?? '',
                    'account_code' => $validated['account_code'] ?? null,
                    'account_name' => $validated['account_name'] ?? null,
                    'updated_at' => now(),
                ]);

            return response()->json([
                'success' => true,
                'message' => '수동 거래가 수정되었습니다.',
            ]);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'success' => false,
                'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.',
            ], 404);
        } catch (\Illuminate\Validation\ValidationException $e) {
            return response()->json([
                'success' => false,
                'error' => '입력 데이터가 올바르지 않습니다.',
                'errors' => $e->errors(),
            ], 422);
        } catch (\Throwable $e) {
            Log::error('수동 거래 수정 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '수정 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 수동 거래 삭제
     */
    public function destroyManual(int $id): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);

            $transaction = BankTransaction::where('id', $id)
                ->where('tenant_id', $tenantId)
                ->where('is_manual', true)
                ->firstOrFail();

            $transaction->delete();

            return response()->json([
                'success' => true,
                'message' => '수동 거래가 삭제되었습니다.',
            ]);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'success' => false,
                'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.',
            ], 404);
        } catch (\Throwable $e) {
            Log::error('수동 거래 삭제 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '삭제 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 분개 내역 조회
     */
    public function splits(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $startDate = $request->input('startDate', date('Ymd'));
            $endDate = $request->input('endDate', date('Ymd'));

            $splits = BankTransactionSplit::getByDateRange($tenantId, $startDate, $endDate);

            return response()->json([
                'success' => true,
                'data' => $splits,
            ]);
        } catch (\Throwable $e) {
            Log::error('계좌 분개 내역 조회 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '조회 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 분개 저장
     */
    public function saveSplits(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $uniqueKey = $request->input('uniqueKey');
            $originalData = $request->input('originalData', []);
            $splits = $request->input('splits', []);

            if (empty($uniqueKey)) {
                return response()->json([
                    'success' => false,
                    'error' => '고유키가 없습니다.',
                ]);
            }

            // 분개 금액 합계 검증
            $originalAmount = floatval($originalData['originalAmount'] ?? 0);
            $splitTotal = array_sum(array_map(function ($s) {
                return floatval($s['amount'] ?? 0);
            }, $splits));

            if (abs($originalAmount - $splitTotal) > 0.01) {
                return response()->json([
                    'success' => false,
                    'error' => "분개 금액 합계({$splitTotal})가 원본 금액({$originalAmount})과 일치하지 않습니다.",
                ]);
            }

            DB::beginTransaction();

            BankTransactionSplit::saveSplits($tenantId, $uniqueKey, $originalData, $splits);

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => '분개가 저장되었습니다.',
                'splitCount' => count($splits),
            ]);
        } catch (\Throwable $e) {
            DB::rollBack();
            Log::error('계좌 분개 저장 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '저장 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 분개 삭제 (원본으로 복원)
     */
    public function deleteSplits(Request $request): JsonResponse
    {
        try {
            $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
            $uniqueKey = $request->input('uniqueKey');

            if (empty($uniqueKey)) {
                return response()->json([
                    'success' => false,
                    'error' => '고유키가 없습니다.',
                ]);
            }

            $deleted = BankTransactionSplit::deleteSplits($tenantId, $uniqueKey);

            return response()->json([
                'success' => true,
                'message' => '분개가 삭제되었습니다.',
                'deleted' => $deleted,
            ]);
        } catch (\Throwable $e) {
            Log::error('계좌 분개 삭제 오류: '.$e->getMessage());

            return response()->json([
                'success' => false,
                'error' => '삭제 오류: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 병합된 로그에서 수동입력 건의 잔액을 직전 거래 기준으로 재계산
     * 로그는 날짜 내림차순(DESC) 정렬 상태로 전달됨
     */
    /**
     * 조회기간 직전의 마지막 잔액 조회
     * API 데이터는 잔액이 정확하므로 그대로 사용,
     * 수동입력은 잔액이 0일 수 있으므로 입출금 누적으로 계산
     */
    private function findBaseBalance(int $tenantId, string $startDate, ?string $bankAccountNum = null): ?float
    {
        // 조회기간 이전의 모든 거래를 시간순(ASC)으로 조회
        $prevTransactions = BankTransaction::where('tenant_id', $tenantId)
            ->where('trans_date', '<', $startDate)
            ->when($bankAccountNum, fn ($q) => $q->where('bank_account_num', $bankAccountNum))
            ->orderBy('trans_date', 'asc')
            ->orderBy('trans_time', 'asc')
            ->get();

        if ($prevTransactions->isEmpty()) {
            return null;
        }

        // 시간순으로 순회하며 잔액 추적
        $balance = null;
        foreach ($prevTransactions as $tx) {
            if (! $tx->is_manual && (float) $tx->balance != 0) {
                // API 데이터: 바로빌이 제공한 정확한 잔액 사용
                $balance = (float) $tx->balance;
            } else {
                // 수동입력 또는 잔액 0인 건: 이전 잔액에서 입출금 계산
                $prev = $balance ?? 0;
                $balance = $prev + (float) $tx->deposit - (float) $tx->withdraw;
            }
        }

        return $balance;
    }

    /**
     * API 로그와 수동 로그 병합 (중복 제거)
     * 수동 거래와 동일한 API 거래가 있으면 API 거래를 제외하고 수동 거래를 유지
     * 매칭 기준: 계좌번호 + 거래일시 + 입금액 + 출금액 (잔액 제외 - 수동 거래는 재계산됨)
     *
     * @return array ['logs' => array, 'removedDeposit' => float, 'removedWithdraw' => float]
     */
    private function mergeWithDedup(array $apiLogs, array $manualLogs): array
    {
        if (empty($manualLogs)) {
            return ['logs' => $apiLogs, 'removedDeposit' => 0, 'removedWithdraw' => 0];
        }

        // 수동 거래의 매칭 키 생성 (잔액 제외)
        $manualKeys = [];
        foreach ($manualLogs as $mLog) {
            $key = implode('|', [
                $mLog['bankAccountNum'] ?? '',
                ($mLog['transDate'] ?? '').($mLog['transTime'] ?? ''),
                (int) ($mLog['deposit'] ?? 0),
                (int) ($mLog['withdraw'] ?? 0),
            ]);
            $manualKeys[$key] = true;
        }

        // API 로그에서 수동 거래와 중복되는 것 제외
        $dedupedApiLogs = [];
        $removedDeposit = 0;
        $removedWithdraw = 0;
        foreach ($apiLogs as $aLog) {
            $key = implode('|', [
                $aLog['bankAccountNum'] ?? '',
                ($aLog['transDate'] ?? '').($aLog['transTime'] ?? ''),
                (int) ($aLog['deposit'] ?? 0),
                (int) ($aLog['withdraw'] ?? 0),
            ]);
            if (isset($manualKeys[$key])) {
                $removedDeposit += (float) ($aLog['deposit'] ?? 0);
                $removedWithdraw += (float) ($aLog['withdraw'] ?? 0);

                continue; // 수동 거래가 우선, API 거래 스킵
            }
            $dedupedApiLogs[] = $aLog;
        }

        if ($removedDeposit > 0 || $removedWithdraw > 0) {
            Log::info('[Eaccount] 중복 거래 제거', [
                'count' => count($manualLogs) - count($dedupedApiLogs) + count($apiLogs) - count($manualLogs),
                'removedDeposit' => $removedDeposit,
                'removedWithdraw' => $removedWithdraw,
            ]);
        }

        return [
            'logs' => array_merge($dedupedApiLogs, $manualLogs),
            'removedDeposit' => $removedDeposit,
            'removedWithdraw' => $removedWithdraw,
        ];
    }

    private function recalcManualBalances(array $logs, ?float $baseBalance = null): array
    {
        if (empty($logs)) {
            return $logs;
        }

        // 시간순(ASC)으로 뒤집어서 순차 처리
        $logs = array_reverse($logs);

        $prevBalance = $baseBalance;
        foreach ($logs as &$log) {
            if (! empty($log['isManual'])) {
                $deposit = (float) ($log['deposit'] ?? 0);
                $withdraw = (float) ($log['withdraw'] ?? 0);
                $newBalance = ($prevBalance !== null ? $prevBalance : 0) + $deposit - $withdraw;
                $log['balance'] = $newBalance;
                $log['balanceFormatted'] = number_format($newBalance);
            }
            $prevBalance = (float) ($log['balance'] ?? 0);
        }
        unset($log);

        // 다시 내림차순(DESC)으로 복원
        return array_reverse($logs);
    }

    /**
     * 수동 입력 건을 API 로그 형식으로 변환
     */
    private function convertManualToLogs($manualTransactions): array
    {
        $logs = [];
        $totalDeposit = 0;
        $totalWithdraw = 0;

        if (! $manualTransactions || $manualTransactions->isEmpty()) {
            return [
                'logs' => [],
                'summary' => [
                    'totalDeposit' => 0,
                    'totalWithdraw' => 0,
                    'count' => 0,
                ],
            ];
        }

        foreach ($manualTransactions as $t) {
            $deposit = (float) $t->deposit;
            $withdraw = (float) $t->withdraw;
            $balance = (float) $t->balance;
            $totalDeposit += $deposit;
            $totalWithdraw += $withdraw;

            $transDt = $t->trans_dt ?? '';
            $dateTime = '';
            if (! empty($transDt) && strlen($transDt) >= 8) {
                $dateTime = substr($transDt, 0, 4).'-'.substr($transDt, 4, 2).'-'.substr($transDt, 6, 2);
                if (strlen($transDt) >= 14) {
                    $dateTime .= ' '.substr($transDt, 8, 2).':'.substr($transDt, 10, 2).':'.substr($transDt, 12, 2);
                }
            }

            $summary = $t->summary ?? '';
            $uniqueKey = implode('|', [
                $t->bank_account_num,
                $transDt,
                (int) $deposit,
                (int) $withdraw,
                (int) $balance,
                $summary,
            ]);

            $logs[] = [
                'transDate' => $t->trans_date,
                'transTime' => $t->trans_time,
                'transDateTime' => $dateTime,
                'bankAccountNum' => $t->bank_account_num,
                'bankName' => $t->bank_name ?? '',
                'deposit' => $deposit,
                'withdraw' => $withdraw,
                'depositFormatted' => number_format($deposit),
                'withdrawFormatted' => number_format($withdraw),
                'balance' => $balance,
                'balanceFormatted' => number_format($balance),
                'summary' => $summary,
                'originalSummary' => $summary,
                'cast' => $t->cast ?? '',
                'originalCast' => $t->cast ?? '',
                'memo' => $t->memo ?? '',
                'transOffice' => $t->trans_office ?? '',
                'accountCode' => $t->account_code ?? '',
                'accountName' => $t->account_name ?? '',
                'clientCode' => $t->client_code ?? '',
                'clientName' => $t->client_name ?? '',
                'isSaved' => true,
                'isOverridden' => false,
                'isManual' => true,
                'dbId' => $t->id,
                'uniqueKey' => $uniqueKey,
            ];
        }

        return [
            'logs' => $logs,
            'summary' => [
                'totalDeposit' => $totalDeposit,
                'totalWithdraw' => $totalWithdraw,
                'count' => count($logs),
            ],
        ];
    }

    /**
     * SOAP 호출
     */
    private function callSoap(string $method, array $params = []): array
    {
        if (! $this->soapClient) {
            return [
                'success' => false,
                'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.',
            ];
        }

        if (empty($this->certKey) && ! $this->isTestMode) {
            return [
                'success' => false,
                'error' => 'CERTKEY가 설정되지 않았습니다.',
            ];
        }

        if (empty($this->corpNum)) {
            return [
                'success' => false,
                'error' => '사업자번호가 설정되지 않았습니다.',
            ];
        }

        // CERTKEY와 CorpNum 자동 추가
        if (! isset($params['CERTKEY'])) {
            $params['CERTKEY'] = $this->certKey ?? '';
        }
        if (! isset($params['CorpNum'])) {
            $params['CorpNum'] = $this->corpNum;
        }

        try {
            Log::info("바로빌 계좌 API 호출 - Method: {$method}, CorpNum: {$this->corpNum}");

            $result = $this->soapClient->$method($params);
            $resultProperty = $method.'Result';

            if (isset($result->$resultProperty)) {
                $resultData = $result->$resultProperty;

                // 에러 코드 체크
                if (is_numeric($resultData) && $resultData < 0) {
                    return [
                        'success' => false,
                        'error' => $this->getErrorMessage((int) $resultData),
                        'error_code' => (int) $resultData,
                    ];
                }

                return [
                    'success' => true,
                    'data' => $resultData,
                ];
            }

            return [
                'success' => true,
                'data' => $result,
            ];
        } catch (\SoapFault $e) {
            Log::error('바로빌 SOAP 오류: '.$e->getMessage());

            return [
                'success' => false,
                'error' => 'SOAP 오류: '.$e->getMessage(),
                'error_code' => $e->getCode(),
            ];
        } catch (\Throwable $e) {
            Log::error('바로빌 API 호출 오류: '.$e->getMessage());

            return [
                'success' => false,
                'error' => 'API 호출 오류: '.$e->getMessage(),
            ];
        }
    }
}
