<?php

require_once __DIR__ . '/ApiIntegrationService.php';

/**
 * Uploads a scanned/photographed document (supplier invoice, quotation, bank
 * deposit slip, or receipt), sends it to whichever AI integration the admin
 * picks (reusing ApiIntegrationService's stored credentials), and lets the
 * admin review/correct the extracted fields before creating the real record
 * via the existing Suppliers/Quotation/Deposit classes.
 */
class DocumentImportService {
    const UPLOAD_DIR = __DIR__ . '/../uploads/document_imports/';

    // Kept well under most providers' request-size ceilings once base64 (~+33%) is applied.
    const MAX_FILE_SIZE = 12 * 1024 * 1024;

    const ALLOWED_MIME_EXT = [
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/webp' => 'webp',
        'application/pdf' => 'pdf',
    ];

    const DOC_TYPE_LABELS = [
        'auto' => 'Auto-detect',
        'supplier_invoice' => 'Supplier Invoice',
        'quotation' => 'Quotation',
        'bank_deposit' => 'Bank Deposit',
        'receipt' => 'Receipt / Proof of Payment',
    ];

    private $conn;
    private $logger;

    public function __construct(PDO $conn, $logger = null) {
        $this->conn = $conn;
        $this->logger = $logger;
        self::ensureTable($conn);
    }

    public static function ensureTable(PDO $conn) {
        $conn->exec(
            'CREATE TABLE IF NOT EXISTS document_imports (' .
            'import_id INT AUTO_INCREMENT PRIMARY KEY,' .
            'branch_id INT NULL,' .
            'uploaded_by INT NOT NULL,' .
            'original_filename VARCHAR(255) NOT NULL,' .
            'stored_filename VARCHAR(100) NOT NULL,' .
            'mime_type VARCHAR(100) NOT NULL,' .
            'file_size INT NOT NULL,' .
            'doc_type_hint VARCHAR(30) NOT NULL DEFAULT "auto",' .
            'detected_doc_type VARCHAR(30) NULL,' .
            'integration_id INT NULL,' .
            'status VARCHAR(20) NOT NULL DEFAULT "uploaded",' .
            'extracted_data LONGTEXT NULL,' .
            'ai_raw_response LONGTEXT NULL,' .
            'error_message VARCHAR(500) NULL,' .
            'target_module VARCHAR(30) NULL,' .
            'target_record_id INT NULL,' .
            'receipt_reference VARCHAR(255) NULL,' .
            'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' .
            'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' .
            'KEY idx_document_imports_status (status),' .
            'KEY idx_document_imports_branch (branch_id)' .
            ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci'
        );
    }

    // ---------------------------------------------------------------
    // Upload
    // ---------------------------------------------------------------

    public function uploadDocument($branchId, $userId, array $file, $docTypeHint, $integrationId) {
        if (empty($file) || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            return ['success' => false, 'message' => 'No file was uploaded.'];
        }
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return ['success' => false, 'message' => 'Upload failed (error code ' . $file['error'] . ').'];
        }
        if ($file['size'] > self::MAX_FILE_SIZE) {
            return ['success' => false, 'message' => 'File exceeds the ' . (self::MAX_FILE_SIZE / 1024 / 1024) . 'MB limit.'];
        }

        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);

        if (!isset(self::ALLOWED_MIME_EXT[$mimeType])) {
            return ['success' => false, 'message' => 'File type not allowed. Upload a JPG, PNG, WEBP, or PDF.'];
        }

        if (!array_key_exists($docTypeHint, self::DOC_TYPE_LABELS)) {
            $docTypeHint = 'auto';
        }

        if (!is_dir(self::UPLOAD_DIR)) {
            mkdir(self::UPLOAD_DIR, 0755, true);
        }

        $extension = self::ALLOWED_MIME_EXT[$mimeType];
        $storedName = bin2hex(random_bytes(16)) . '.' . $extension;
        $destination = self::UPLOAD_DIR . $storedName;

        if (!move_uploaded_file($file['tmp_name'], $destination)) {
            return ['success' => false, 'message' => 'Unable to save uploaded file.'];
        }

        $stmt = $this->conn->prepare(
            'INSERT INTO document_imports ' .
            '(branch_id, uploaded_by, original_filename, stored_filename, mime_type, file_size, doc_type_hint, integration_id, status) ' .
            'VALUES (:branch_id, :uploaded_by, :original_filename, :stored_filename, :mime_type, :file_size, :doc_type_hint, :integration_id, "uploaded")'
        );
        $stmt->execute([
            ':branch_id' => $branchId ? (int)$branchId : null,
            ':uploaded_by' => (int)$userId,
            ':original_filename' => basename($file['name']),
            ':stored_filename' => $storedName,
            ':mime_type' => $mimeType,
            ':file_size' => (int)$file['size'],
            ':doc_type_hint' => $docTypeHint,
            ':integration_id' => $integrationId ? (int)$integrationId : null,
        ]);

        return ['success' => true, 'import_id' => (int)$this->conn->lastInsertId()];
    }

    // ---------------------------------------------------------------
    // AI extraction
    // ---------------------------------------------------------------

    public function processImport($importId) {
        $row = $this->getImport($importId);
        if (!$row) {
            return ['success' => false, 'message' => 'Import not found.'];
        }
        if (!$row['integration_id']) {
            return ['success' => false, 'message' => 'No AI integration was selected for this upload.'];
        }

        $this->updateRow($importId, ['status' => 'processing']);

        try {
            $integration = new ApiIntegrationService($this->conn, $row['integration_id']);
            $settings = $integration->getRawSettings();

            if ($settings['api_key'] === '') {
                throw new Exception('The selected AI integration has no API key configured.');
            }
            if (!$settings['is_enabled']) {
                throw new Exception('The selected AI integration is disabled.');
            }

            $filePath = self::UPLOAD_DIR . $row['stored_filename'];
            if (!is_file($filePath)) {
                throw new Exception('The uploaded file is missing from storage.');
            }

            $providerKey = $settings['provider_key'];
            $mimeType = $row['mime_type'];

            if ($mimeType === 'application/pdf' && $providerKey !== 'anthropic') {
                throw new Exception(
                    'PDF extraction requires a Claude (Anthropic) integration. ' .
                    'Upload an image (JPG/PNG/WEBP) instead, or select/add a Claude integration under Settings > APIs.'
                );
            }

            $base64 = base64_encode(file_get_contents($filePath));
            $prompt = $this->buildPrompt($row['doc_type_hint']);
            $baseUrl = rtrim($settings['api_base_url'] !== '' ? $settings['api_base_url'] : (ApiIntegrationService::PRESETS[$providerKey]['base_url'] ?? ''), '/');
            $model = $settings['model_name'] !== '' ? $settings['model_name'] : (ApiIntegrationService::PRESETS[$providerKey]['model'] ?? '');

            if ($baseUrl === '') {
                throw new Exception('The selected AI integration has no API base URL configured.');
            }

            switch ($providerKey) {
                case 'anthropic':
                    $result = $this->callAnthropic($baseUrl, $settings['api_key'], $model, $mimeType, $base64, $prompt);
                    break;
                case 'openai':
                case 'deepseek':
                case 'custom':
                    $result = $this->callOpenAiCompatible($baseUrl, $settings['api_key'], $model, $mimeType, $base64, $prompt);
                    break;
                default:
                    throw new Exception('Unsupported provider: ' . $providerKey);
            }

            $extracted = $this->extractJson($result['text']);
            if (!is_array($extracted)) {
                $this->updateRow($importId, ['ai_raw_response' => substr((string)$result['raw'], 0, 65000)]);
                $snippet = trim((string)$result['text']);
                $snippet = $snippet !== '' ? substr($snippet, 0, 300) : '(empty response)';
                throw new Exception('The AI response could not be parsed as JSON. Raw response: ' . $snippet);
            }

            $detectedType = is_string($extracted['detected_doc_type'] ?? null) ? $extracted['detected_doc_type'] : 'unknown';

            $this->updateRow($importId, [
                'status' => 'extracted',
                'detected_doc_type' => $detectedType,
                'extracted_data' => json_encode($extracted),
                'ai_raw_response' => substr((string)$result['raw'], 0, 65000),
                'error_message' => null,
            ]);

            return ['success' => true, 'detected_doc_type' => $detectedType, 'extracted_data' => $extracted];
        } catch (Exception $e) {
            $this->updateRow($importId, ['status' => 'failed', 'error_message' => substr($e->getMessage(), 0, 500)]);
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }

    // Fields the confirm step actually needs to create each record type -
    // used to tell the AI what it must not skip, keyed by detected_doc_type.
    const DOC_TYPE_REQUIRED_FIELDS = [
        'supplier_invoice' => [
            'supplier_name' => 'the supplier/vendor company name printed on the invoice',
            'document_number' => 'the invoice number',
            'document_date' => 'the invoice date',
            'due_date' => 'the payment due date, if printed',
            'total' => 'the total amount due',
            'notes' => 'a short description of what was invoiced',
        ],
        'quotation' => [
            'customer_name' => 'the customer/client name the quote is addressed to',
            'customer_contact' => 'the customer phone number or email, if printed',
            'document_number' => 'the quotation/reference number, if printed',
            'line_items' => 'EVERY line item on the document, each with item_name, qty, and unit_price - do not summarize or skip any row',
            'subtotal' => 'the subtotal before tax',
            'tax' => 'the tax amount',
            'total' => 'the grand total',
        ],
        'bank_deposit' => [
            'bank_name' => 'the bank name printed on the deposit slip',
            'account_reference' => 'the account number or reference printed on the slip',
            'document_date' => 'the deposit date',
            'total' => 'the deposited amount',
        ],
        'receipt' => [
            'payer_name' => 'the name of the person/customer who paid',
            'payment_method' => 'how they paid (cash, bank transfer, mobile money, etc.)',
            'document_date' => 'the payment date',
            'document_number' => 'the receipt number, if printed',
            'total' => 'the amount paid',
        ],
    ];

    private function buildPrompt($docTypeHint) {
        $hintLine = '';
        $requiredLine = '';

        if ($docTypeHint !== 'auto' && isset(self::DOC_TYPE_REQUIRED_FIELDS[$docTypeHint])) {
            $hintLine = 'The uploader indicated this is likely a: ' . self::DOC_TYPE_LABELS[$docTypeHint] . ". Still verify against the document itself.\n";
            $requiredLine = "If it is indeed a " . self::DOC_TYPE_LABELS[$docTypeHint] . ", you MUST make every effort to capture these fields " .
                "(only use null if the value truly is not printed anywhere on the document):\n" .
                $this->describeRequiredFields(self::DOC_TYPE_REQUIRED_FIELDS[$docTypeHint]);
        } else {
            $requiredLine = "Whichever type you detect, these fields are required for that type and must not be skipped " .
                "(only use null if the value truly is not printed anywhere on the document):\n";
            foreach (self::DOC_TYPE_REQUIRED_FIELDS as $type => $fields) {
                $requiredLine .= "- If " . $type . ": " . implode('; ', array_keys($fields)) . "\n";
            }
        }

        return
            "You are extracting structured data from a photographed or scanned business document for a point-of-sale system.\n" .
            $hintLine .
            "Classify the document as exactly one of: supplier_invoice, quotation, bank_deposit, receipt, unknown.\n" .
            $requiredLine .
            "Respond with ONLY a single JSON object (no markdown, no commentary) using this shape " .
            "(fields not listed as required above may be omitted if they don't apply, use null when a value isn't visible):\n" .
            "{\n" .
            "  \"detected_doc_type\": \"supplier_invoice|quotation|bank_deposit|receipt|unknown\",\n" .
            "  \"supplier_name\": string|null,\n" .
            "  \"customer_name\": string|null,\n" .
            "  \"customer_contact\": string|null,\n" .
            "  \"document_number\": string|null,\n" .
            "  \"document_date\": \"YYYY-MM-DD\"|null,\n" .
            "  \"due_date\": \"YYYY-MM-DD\"|null,\n" .
            "  \"line_items\": [ { \"item_name\": string, \"qty\": number, \"unit_price\": number } ],\n" .
            "  \"subtotal\": number|null,\n" .
            "  \"tax\": number|null,\n" .
            "  \"total\": number|null,\n" .
            "  \"bank_name\": string|null,\n" .
            "  \"account_reference\": string|null,\n" .
            "  \"payer_name\": string|null,\n" .
            "  \"payment_method\": string|null,\n" .
            "  \"notes\": string|null\n" .
            "}\n";
    }

    private function describeRequiredFields(array $fields) {
        $lines = '';
        foreach ($fields as $name => $description) {
            $lines .= "- $name: $description\n";
        }
        return $lines;
    }

    private function callAnthropic($baseUrl, $apiKey, $model, $mimeType, $base64, $prompt) {
        $model = $model !== '' ? $model : 'claude-opus-4-8';
        $documentBlock = $mimeType === 'application/pdf'
            ? ['type' => 'document', 'source' => ['type' => 'base64', 'media_type' => $mimeType, 'data' => $base64]]
            : ['type' => 'image', 'source' => ['type' => 'base64', 'media_type' => $mimeType, 'data' => $base64]];

        $payload = json_encode([
            'model' => $model,
            'max_tokens' => 4096,
            'messages' => [[
                'role' => 'user',
                'content' => [$documentBlock, ['type' => 'text', 'text' => $prompt]],
            ]],
        ]);

        $response = $this->curlRequest($baseUrl . '/v1/messages', $payload, [
            'x-api-key: ' . $apiKey,
            'anthropic-version: 2023-06-01',
            'content-type: application/json',
        ]);

        $decoded = $this->decodeOrThrow($response, 'Claude (Anthropic)');
        $text = '';
        foreach ((array)($decoded['content'] ?? []) as $block) {
            if (is_array($block) && ($block['type'] ?? '') === 'text') {
                $text .= $block['text'] ?? '';
            }
        }
        return ['text' => $text, 'raw' => $response['body']];
    }

    private function callOpenAiCompatible($baseUrl, $apiKey, $model, $mimeType, $base64, $prompt) {
        $model = $model !== '' ? $model : 'gpt-4o';
        $payload = json_encode([
            'model' => $model,
            'max_tokens' => 4096,
            'messages' => [[
                'role' => 'user',
                'content' => [
                    ['type' => 'text', 'text' => $prompt],
                    ['type' => 'image_url', 'image_url' => ['url' => 'data:' . $mimeType . ';base64,' . $base64]],
                ],
            ]],
        ]);

        $response = $this->curlRequest($baseUrl . '/chat/completions', $payload, [
            'Authorization: Bearer ' . $apiKey,
            'content-type: application/json',
        ]);

        $decoded = $this->decodeOrThrow($response, 'the AI provider');
        $text = $decoded['choices'][0]['message']['content'] ?? '';
        return ['text' => $text, 'raw' => $response['body']];
    }

    private function decodeOrThrow(array $response, $label) {
        if ($response['curl_error']) {
            throw new Exception("Connection to $label failed: " . $response['curl_error']);
        }
        if ($response['http_code'] < 200 || $response['http_code'] >= 300) {
            $decoded = json_decode((string)$response['body'], true);
            $errMsg = is_array($decoded) ? ($decoded['error']['message'] ?? null) : null;
            throw new Exception($errMsg ?: "$label request failed (HTTP {$response['http_code']}).");
        }
        $decoded = json_decode((string)$response['body'], true);
        if (!is_array($decoded)) {
            throw new Exception("$label returned an unreadable response.");
        }
        return $decoded;
    }

    private function curlRequest($url, $payload, $headers) {
        if (!function_exists('curl_init')) {
            throw new Exception('The PHP cURL extension is not enabled on this server.');
        }
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 60,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        $body = curl_exec($ch);
        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        return ['body' => $body, 'http_code' => $httpCode, 'curl_error' => $error];
    }

    /** Strips ```json fences if present and returns the decoded array, or null. */
    private function extractJson($text) {
        $text = trim((string)$text);
        if (preg_match('/```(?:json)?\s*(.*?)\s*```/s', $text, $m)) {
            $text = trim($m[1]);
        }
        $start = strpos($text, '{');
        $end = strrpos($text, '}');
        if ($start === false || $end === false || $end < $start) {
            return null;
        }
        $decoded = json_decode(substr($text, $start, $end - $start + 1), true);
        return is_array($decoded) ? $decoded : null;
    }

    // ---------------------------------------------------------------
    // Confirm / reject / delete
    // ---------------------------------------------------------------

    public function confirmImport($importId, $targetModule, array $fields, $userId, $branchId) {
        $row = $this->getImport($importId);
        if (!$row) {
            return ['success' => false, 'message' => 'Import not found.'];
        }
        if (!in_array($row['status'], ['extracted', 'failed'], true)) {
            return ['success' => false, 'message' => 'This import has already been ' . $row['status'] . '.'];
        }

        try {
            switch ($targetModule) {
                case 'supplier_invoice':
                    $recordId = $this->confirmSupplierInvoice($fields, $userId);
                    break;
                case 'quotation':
                    $recordId = $this->confirmQuotation($fields, $userId, $branchId);
                    break;
                case 'deposit':
                    $recordId = $this->confirmDeposit($fields, $userId, $branchId);
                    break;
                case 'receipt':
                    $recordId = null;
                    break;
                default:
                    throw new Exception('Unknown target module: ' . $targetModule);
            }
        } catch (Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }

        $this->updateRow($importId, [
            'status' => 'confirmed',
            'target_module' => $targetModule,
            'target_record_id' => $recordId,
            'receipt_reference' => $targetModule === 'receipt' ? substr((string)($fields['receipt_reference'] ?? ''), 0, 255) : $row['receipt_reference'],
        ]);

        if ($this->logger) {
            $this->logger->logAdmin('DOCUMENT_IMPORT_CONFIRM', "Confirmed import #$importId into $targetModule" . ($recordId ? " (record #$recordId)" : ''), $recordId);
        }

        return ['success' => true, 'target_record_id' => $recordId];
    }

    private function confirmSupplierInvoice(array $fields, $userId) {
        require_once __DIR__ . '/Suppliers.php';
        $supplierId = (int)($fields['supplier_id'] ?? 0);
        if ($supplierId <= 0) {
            throw new Exception('Select the supplier this invoice belongs to.');
        }
        $suppliers = new Suppliers($this->conn);
        return $suppliers->recordInvoice($supplierId, [
            'invoice_number' => trim((string)($fields['document_number'] ?? '')),
            'invoice_date' => $fields['document_date'] ?? date('Y-m-d'),
            'due_date' => $fields['due_date'] ?? null,
            'description' => trim((string)($fields['notes'] ?? '')),
            'amount' => (float)($fields['total'] ?? 0),
        ], (int)$userId);
    }

    private function confirmQuotation(array $fields, $userId, $branchId) {
        require_once __DIR__ . '/Quotation.php';
        $quotation = new Quotation($this->conn, $this->logger);

        $items = [];
        foreach ((array)($fields['line_items'] ?? []) as $item) {
            $items[] = [
                'item_name' => trim((string)($item['item_name'] ?? '')),
                'qty' => (int)($item['qty'] ?? 0),
                'unit_price' => (float)($item['unit_price'] ?? 0),
            ];
        }

        $quotationNumber = trim((string)($fields['document_number'] ?? '')) ?: ('QT-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -4)));

        $result = $quotation->createQuotation(
            (int)$branchId,
            (int)$userId,
            $quotationNumber,
            trim((string)($fields['customer_name'] ?? '')),
            trim((string)($fields['customer_contact'] ?? '')),
            $items,
            (float)($fields['subtotal'] ?? 0),
            (float)($fields['tax'] ?? 0),
            (float)($fields['total'] ?? 0),
            (int)($fields['validity_days'] ?? 30),
            trim((string)($fields['notes'] ?? ''))
        );

        if (!$result['success']) {
            throw new Exception($result['message']);
        }
        return (int)$result['quotation_id'];
    }

    private function confirmDeposit(array $fields, $userId, $branchId) {
        require_once __DIR__ . '/Deposit.php';
        $deposit = new Deposit($this->conn, $this->logger);

        $amount = (float)($fields['total'] ?? 0);
        if ($amount <= 0) {
            throw new Exception('Deposit amount must be greater than zero.');
        }

        $notesParts = array_filter([
            trim((string)($fields['bank_name'] ?? '')),
            trim((string)($fields['account_reference'] ?? '')),
            trim((string)($fields['notes'] ?? '')),
        ]);

        $depositId = $deposit->addDeposit(
            (int)$branchId,
            $fields['document_date'] ?? date('Y-m-d'),
            $amount,
            implode(' - ', $notesParts),
            (int)$userId
        );

        if (!$depositId) {
            throw new Exception('Failed to record the deposit.');
        }
        return (int)$depositId;
    }

    public function rejectImport($importId) {
        $row = $this->getImport($importId);
        if (!$row) {
            return ['success' => false, 'message' => 'Import not found.'];
        }
        $this->updateRow($importId, ['status' => 'rejected']);
        return ['success' => true];
    }

    public function deleteImport($importId) {
        $row = $this->getImport($importId);
        if (!$row) {
            return ['success' => false, 'message' => 'Import not found.'];
        }
        if ($row['status'] === 'confirmed') {
            return ['success' => false, 'message' => 'Reject an import before deleting it (this one already created a record).'];
        }

        $filePath = self::UPLOAD_DIR . $row['stored_filename'];
        if (is_file($filePath)) {
            @unlink($filePath);
        }

        $stmt = $this->conn->prepare('DELETE FROM document_imports WHERE import_id = :id');
        $stmt->execute([':id' => (int)$importId]);
        return ['success' => true];
    }

    // ---------------------------------------------------------------
    // Reads
    // ---------------------------------------------------------------

    public function listImports($branchId = null, $status = null) {
        $sql = 'SELECT di.*, u.username AS uploaded_by_name FROM document_imports di ' .
               'LEFT JOIN users u ON di.uploaded_by = u.user_id WHERE 1=1';
        $params = [];
        if ($branchId) {
            $sql .= ' AND di.branch_id = :branch_id';
            $params[':branch_id'] = (int)$branchId;
        }
        if ($status) {
            $sql .= ' AND di.status = :status';
            $params[':status'] = $status;
        }
        $sql .= ' ORDER BY di.created_at DESC LIMIT 200';

        $stmt = $this->conn->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function getImport($importId) {
        $stmt = $this->conn->prepare('SELECT * FROM document_imports WHERE import_id = :id');
        $stmt->execute([':id' => (int)$importId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    private function updateRow($importId, array $fields) {
        $set = [];
        $params = [':id' => (int)$importId];
        foreach ($fields as $col => $val) {
            $set[] = "$col = :$col";
            $params[":$col"] = $val;
        }
        $stmt = $this->conn->prepare('UPDATE document_imports SET ' . implode(', ', $set) . ' WHERE import_id = :id');
        $stmt->execute($params);
    }
}
