<?php

namespace App\Libraries\undeposited_funds;

use App\Models\UndepositedFundsModel;

/**
 * Low‑level QuickBooks helpers:
 *   • query() wrappers
 *   • pull*() routines for each QBO object
 *   • createDeposit() – builds & posts a Deposit with LinkedTxn lines
 */
class UndepositedFundsLibrary
{
    /* ------------------------------------------------------------------
       Generic helpers
       ------------------------------------------------------------------ */
    private function strContainsUndeposited(?string $s): bool
    {
        return $s && stripos($s, 'undeposited') !== false;
    }

    private function runQuery(string $realm, string $tok, string $q): array
    {
        $url = "https://quickbooks.api.intuit.com/v3/company/{$realm}/query?query=" . urlencode($q);
        $ch  = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_HTTPHEADER     => [
                "Authorization: Bearer {$tok}",
                'Accept: application/json',
                'Content-Type: application/text',
            ],
            CURLOPT_RETURNTRANSFER => true,
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($code !== 200) {
            throw new \RuntimeException("QBO API error {$code}: {$resp}");
        }
        return json_decode($resp, true);
    }

    /* ------------------------------------------------------------------
       Pull Deposit masters (for cross‑reference)
       ------------------------------------------------------------------ */
    public function pullDeposits(string $realm, string $tok, string $from, string $to): void
    {
        $uf = new UndepositedFundsModel();
        $start = 1; $max = 1000;

        do {
            $q = "
              SELECT *
              FROM Deposit
              WHERE TxnDate >= '{$from}' AND TxnDate <= '{$to}'
              ORDERBY Id STARTPOSITION {$start} MAXRESULTS {$max}";
            $rows = $this->runQuery($realm, $tok, $q)['QueryResponse']['Deposit'] ?? [];
            foreach ($rows as $d) { $uf->upsertDeposit($d); }
            $start += $max;
        } while (count($rows) === $max);
    }

    /* ------------------------------------------------------------------
       Pull each UF‑eligible object type
       ------------------------------------------------------------------ */
    public function pullPayments(string $realm, string $tok, string $from, string $to, UndepositedFundsModel $uf): void
    {
        $this->genericPull(
            'Payment', $realm, $tok, $from, $to,
            static fn(array $p): bool => true,
            $uf
        );
    }

    public function pullSalesReceipts(string $realm, string $tok, string $from, string $to, UndepositedFundsModel $uf): void
    {
        $this->genericPull(
            'SalesReceipt', $realm, $tok, $from, $to,
            fn(array $sr): bool => $this->strContainsUndeposited($sr['DepositToAccountRef']['name'] ?? ''),
            $uf
        );
    }

    public function pullRefundReceipts(string $realm, string $tok, string $from, string $to, UndepositedFundsModel $uf): void
    {
        $this->genericPull(
            'RefundReceipt', $realm, $tok, $from, $to,
            fn(array $rr): bool => $this->strContainsUndeposited(
                $rr['RefundFromAccountRef']['name'] ?? $rr['DepositToAccountRef']['name'] ?? ''
            ),
            $uf
        );
    }

    public function pullCreditMemos(string $realm, string $tok, string $from, string $to, UndepositedFundsModel $uf): void
    {
        $this->genericPull(
            'CreditMemo', $realm, $tok, $from, $to,
            fn(array $cm): bool => $this->strContainsUndeposited($cm['DepositToAccountRef']['name'] ?? ''),
            $uf
        );
    }

    public function pullJournalEntries(string $realm, string $tok, string $from, string $to, UndepositedFundsModel $uf): void
    {
        $this->genericPull(
            'JournalEntry', $realm, $tok, $from, $to,
            function (array $je): bool {
                foreach ($je['Line'] ?? [] as $ln) {
                    $ref = $ln['JournalEntryLineDetail']['AccountRef'] ?? [];
                    if ($this->strContainsUndeposited($ref['name'] ?? '')) { return true; }
                }
                return false;
            },
            $uf
        );
    }

    /* ---------- shared pull implementation ---------- */
    private function genericPull(
        string $type,
        string $realm,
        string $tok,
        string $from,
        string $to,
        callable $qualifier,
        UndepositedFundsModel $uf
    ): void {
        $start = 1; $max = 1000;
        do {
            $q = "
              SELECT *
              FROM {$type}
              WHERE TxnDate >= '{$from}' AND TxnDate <= '{$to}'
              ORDERBY Id STARTPOSITION {$start} MAXRESULTS {$max}";
            $rows = $this->runQuery($realm, $tok, $q)['QueryResponse'][$type] ?? [];
            foreach ($rows as $row) {
                if ($qualifier($row)) { $uf->upsertUndepositedRecord($row, $type); }
            }
            $start += $max;
        } while (count($rows) === $max);
    }

    /* ------------------------------------------------------------------
       Create a Deposit with fully linked lines
       ------------------------------------------------------------------ */
    public function createDeposit(array $ufRows, string $realmId, string $accessToken): array
    {
        if ($ufRows === []) {
            throw new \InvalidArgumentException('No UF rows supplied');
        }

        /* ---- Prepare Deposit header ---- */
        $depositAcct = [
            'value' => $ufRows[0]['deposit_to_account_ref_value'] ?? '43',
            'name'  => $ufRows[0]['deposit_to_account_ref_name']  ?? 'Checking',
        ];

        $payload = [
            'TxnDate'             => date('Y-m-d'),
            'PrivateNote'         => 'Created from Brechbill Deposits tool',
            'DepositToAccountRef' => $depositAcct,
            'Line'                => [],
        ];

        $missing = [];

        /* ---- Build each linked‑txn line ---- */
        foreach ($ufRows as $row) {
            $amount = ($row['txn_type'] === 'JournalEntry')
                ? ((float)$row['debit_amount'] ?: (float)$row['credit_amount'])
                : (float)$row['amount'];
            if($row['txn_type'] != 'RefundReceipt') $amount = abs($amount);                // QBO expects positive
            $desc = !empty($row['memo']) ? $row['memo'] : (!empty($row['je_description']) ? $row['je_description'] : '');

            $ufAcct = [
                'value' => $row['undeposited_acct_id']   ?? '112',
                'name'  => $row['undeposited_acct_name'] ?? 'Undeposited Funds',
            ];

            /* ---- every selected UF row must have its txn_line_ids ---- */
            $lineIds = $row['txn_line_ids'] ?? [];

            /* Fallback look‑up (rare) */
            if ($lineIds === []) {
                $found = $this->lookupTxnLineId($row['qb_txn_id'], $row['line_items_json'] ?? null);
                if ($found !== null) { $lineIds[] = $found; }
            }

            if ($lineIds === []) {
                $missing[] = [
                    'qb_txn_id' => $row['qb_txn_id'],
                    'txn_type'  => $row['txn_type'],
                    'customer'  => $row['customer_name'] ?? $row['je_line_name'] ?? '',
                    'reason'    => 'Missing TxnLineId',
                ];
                continue;
            }

            // foreach ($lineIds as $lnId) {
                $payload['Line'][] = [
                    'Amount'      => $amount,
                    'Description' => $desc,
                    'LinkedTxn'   => [[
                        'TxnId'     => $row['qb_txn_id'],
                        'TxnLineId' => "0",
                        'TxnType'   => $row['txn_type']
                    ]],
                    'DepositLineDetail' => [
                        'CheckNum' => $row['reference_number']
                    ]
                ];
            // }
        }

        /* ---- Abort if anything is missing ---- */
        if ($missing !== []) {
            throw new \RuntimeException(
                'Deposit NOT created. The following item(s) are incomplete: '
                . json_encode($missing, JSON_PRETTY_PRINT)
            );
        }

        if ($payload['Line'] === []) {
            throw new \RuntimeException('No valid Line items to deposit.');
        }

        log_message('error', '[DEPOSIT‑PAYLOAD] ' . json_encode($payload));

        /* ---- POST to QBO ---- */
        $ch = curl_init("https://quickbooks.api.intuit.com/v3/company/{$realmId}/deposit");
        curl_setopt_array($ch, [
            CURLOPT_HTTPHEADER     => [
                "Authorization: Bearer {$accessToken}",
                'Accept: application/json',
                'Content-Type: application/json',
            ],
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_RETURNTRANSFER => true,
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if (! in_array($code, [200, 201], true)) {
            throw new \RuntimeException("QBO Deposit error {$code}: {$resp}");
        }

        return [
            'payload'  => $payload,
            'response' => json_decode($resp, true),
        ];
    }

    /* ------------------------------------------------------------------
       Graceful fallback line‑id lookup (DB or embedded JSON)
       ------------------------------------------------------------------ */
    private function lookupTxnLineId(string $qbId, ?string $lineJson): ?string
    {
        $db  = db_connect();
        $row = $db->table('undeposited_funds uf')
                  ->select('utl.txn_line_id')
                  ->join('undeposited_funds_txn_lines utl', 'utl.uf_id = uf.id', 'left')
                  ->where('uf.qb_txn_id', $qbId)
                  ->limit(1)
                  ->get()
                  ->getRowArray();
        if ($row && $row['txn_line_id'] !== '') {
            return (string) $row['txn_line_id'];
        }

        if ($lineJson) {
            foreach (json_decode($lineJson, true) ?: [] as $ln) {
                if (isset($ln['Id']) && $ln['Id'] !== '') {
                    return (string) $ln['Id'];
                }
            }
        }

        return null;
    }
}
