<?php
namespace Modules\Produtos\Controllers;

use Gestoreasy\Core\Http\Request;
use Gestoreasy\Core\Http\Response;
use Gestoreasy\Core\Tenant\ConnectionManager;
use Gestoreasy\Core\Validation\Br;
use Gestoreasy\Core\Audit\AuditLogger;
use Gestoreasy\Core\View\View;

class ProductsController {
    // Checar unicidade de código (SKU)
    public function checkCodigo(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $codigo = trim((string)($r->query['codigo'] ?? ''));
        if ($codigo === '') return Response::json(['exists' => false]);
        $stmt = $pdo->prepare('SELECT id FROM products WHERE codigo = ? LIMIT 1');
        $stmt->execute([$codigo]);
        $exists = $stmt->fetch() ? true : false;
        return Response::json(['exists' => $exists]);
    }
    public function list(Request $r): Response {
        try {
            $pdo = ConnectionManager::forTenant();
            $page = max(1, (int)($r->query['page'] ?? 1));
            $per = min(100, max(1, (int)($r->query['per_page'] ?? 20)));
            $q = trim((string)($r->query['q'] ?? ''));
            $where = '';
            $params = [];

            // Verificar se a tabela product_meta existe (evitar falha em tenants sem migração aplicada)
            $hasMeta = false;
            try {
                $chk = $pdo->prepare("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'product_meta'");
                $chk->execute();
                $hasMeta = ((int)($chk->fetch()['c'] ?? 0)) > 0;
            } catch (\Throwable $eChk) {
                // Ambientes sem information_schema (ex.: SQLite) ou permissões: assumir false
                $hasMeta = false;
            }

            // Busca ampliada: código/EAN/NCM e metadados (nome, marca, categoria, SKU)
            if ($q !== '') {
                $like = "%$q%";
                if ($hasMeta) {
                    // meta_json é coluna JSON; em MySQL não é permitido usar LIKE diretamente.
                    // Fazemos CAST para CHAR para permitir busca textual ampla.
                    $where = 'WHERE p.codigo LIKE ? OR p.ean LIKE ? OR p.ncm LIKE ? OR CAST(pm.meta_json AS CHAR) LIKE ?';
                    $params = [$like, $like, $like, $like];
                } else {
                    // Sem tabela de meta, busca somente em colunas de products
                    $where = 'WHERE p.codigo LIKE ? OR p.ean LIKE ? OR p.ncm LIKE ?';
                    $params = [$like, $like, $like];
                }
            }

            $offset = ($page - 1) * $per;
            $join = $hasMeta ? 'LEFT JOIN product_meta pm ON pm.product_id = p.id' : '';
            $selectMeta = $hasMeta ? ', pm.meta_json' : '';
            $sql = "SELECT p.id, p.codigo, p.ean, p.ncm, p.cest, p.cfop, p.preco, p.estoque, p.location_corredor, p.location_prateleira, p.location_posicao" . $selectMeta . "
                    FROM products p
                    $join
                    $where
                    ORDER BY p.id DESC
                    LIMIT $per OFFSET $offset";
            $stmt = $pdo->prepare($sql);
            $stmt->execute($params);
            $rows = $stmt->fetchAll();
            // Enriquecer com campo nome a partir de meta_json (quando existir)
            if ($hasMeta && is_array($rows)) {
                foreach ($rows as &$row) {
                    $nome = '';
                    try {
                        if (isset($row['meta_json']) && $row['meta_json'] !== null) {
                            $meta = json_decode((string)$row['meta_json'], true);
                            if (is_array($meta)) {
                                $nome = (string)($meta['nome'] ?? $meta['name'] ?? $meta['title'] ?? '');
                            }
                        }
                    } catch (\Throwable $e) { /* ignore parse errors */ }
                    $row['nome'] = $nome;
                }
                unset($row);
            }

            $countSql = "SELECT COUNT(*) AS c
                         FROM products p
                         $join
                         $where";
            $countStmt = $pdo->prepare($countSql);
            $countStmt->execute($params);
            $totalRow = $countStmt->fetch();
            $total = (int)($totalRow['c'] ?? 0);
            $totalPages = (int)ceil($total / $per);
            AuditLogger::logRead('products', null, ['endpoint' => 'produtos.list', 'q' => $q, 'page' => $page, 'per_page' => $per, 'has_meta' => $hasMeta]);
            // Desabilitar cache para garantir que a grid reflita alterações imediatamente
            return new Response(200,
                ['data' => $rows, 'page' => $page, 'per_page' => $per, 'total' => $total, 'total_pages' => $totalPages],
                [
                    'Content-Type' => 'application/json',
                    'Cache-Control' => 'no-store, no-cache, must-revalidate',
                    'Pragma' => 'no-cache'
                ]
            );
        } catch (\Throwable $e) {
            // Não quebrar a aplicação: retornar erro estruturado e registrar contexto mínimo
            error_log('[produtos.list] Falha na listagem: ' . $e->getMessage());
            return new Response(500, ['error' => ['code' => 'products_list_failed', 'message' => $e->getMessage()]]);
        }
    }

    // Definir imagem principal do produto (position = 0)
    public function setPrimaryMedia(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $productId = (int)($params[0] ?? 0);
        if ($productId <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        $body = $r->body ?? [];
        $filename = (string)($body['filename'] ?? '');
        $url = (string)($body['url'] ?? '');
        if ($filename === '' && $url === '') return new Response(422, ['error' => ['code' => 'invalid_payload', 'message' => 'Informe filename ou url']]);
        // Verificar existência do produto
        $chk = $pdo->prepare('SELECT id FROM products WHERE id = ? LIMIT 1');
        $chk->execute([$productId]);
        if (!$chk->fetch()) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Produto não encontrado']]);
        // Localizar mídia
        $findSql = 'SELECT id FROM product_media WHERE product_id = ? AND type = "image" AND (' . ($filename !== '' ? 'filename = ?' : 'url = ?') . ') LIMIT 1';
        $findStmt = $pdo->prepare($findSql);
        $findStmt->execute([$productId, $filename !== '' ? $filename : $url]);
        $media = $findStmt->fetch();
        if (!$media) return new Response(404, ['error' => ['code' => 'media_not_found', 'message' => 'Imagem não encontrada']]);
        $mediaId = (int)$media['id'];
        try {
            $pdo->beginTransaction();
            // Colocar a mídia selecionada como principal (position = 0)
            $pdo->prepare('UPDATE product_media SET position = 0 WHERE id = ?')->execute([$mediaId]);
            // Ajustar as demais imagens para posição 1 (mantém simples e estável)
            $pdo->prepare('UPDATE product_media SET position = 1 WHERE product_id = ? AND type = "image" AND id <> ?')->execute([$productId, $mediaId]);
            $pdo->commit();
            AuditLogger::logUpdate('product_media', $productId, [], ['primary_media_id' => $mediaId], ['endpoint' => 'produtos.media_primary']);
            return Response::json(['ok' => true]);
        } catch (\Throwable $e) {
            if ($pdo->inTransaction()) { $pdo->rollBack(); }
            return new Response(500, ['error' => ['code' => 'media_primary_failed', 'message' => $e->getMessage()]]);
        }
    }

    public function getById(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $id = (int)($params[0] ?? 0);
        if ($id <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        $stmt = $pdo->prepare('SELECT id, codigo, ean, ncm, cest, cfop, preco, estoque, created_at, updated_at FROM products WHERE id = ? LIMIT 1');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        if (!$row) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Produto não encontrado']]);
        AuditLogger::logRead('products', $id, ['endpoint' => 'produtos.get_by_id']);
        $etag = '"' . sha1(json_encode($row, JSON_UNESCAPED_UNICODE)) . '"';
        $ifNoneMatch = $r->header('if-none-match');
        $ts = $row['updated_at'] ?? $row['created_at'] ?? null;
        $lastModified = $ts ? gmdate('D, d M Y H:i:s', strtotime($ts)) . ' GMT' : null;
        if ($ifNoneMatch && $ifNoneMatch === $etag) {
            $headers = ['ETag' => $etag];
            if ($lastModified) $headers['Last-Modified'] = $lastModified;
            return new Response(304, '', $headers);
        }
        $ifModifiedSince = $r->header('if-modified-since');
        if ($ifModifiedSince && $lastModified) {
            $sinceTs = strtotime($ifModifiedSince);
            $resTs = strtotime($lastModified);
            if ($sinceTs !== false && $resTs !== false && $sinceTs >= $resTs) {
                $headers = ['ETag' => $etag];
                $headers['Last-Modified'] = $lastModified;
                return new Response(304, '', $headers);
            }
        }
        $headers = ['ETag' => $etag, 'Content-Type' => 'application/json'];
        if ($lastModified) $headers['Last-Modified'] = $lastModified;
        return new Response(200, ['data' => $row], $headers);
    }

    public function create(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $data = $r->body;
        $codigo = $data['codigo'] ?? '';
        $ncm = $data['ncm'] ?? null;
        $ean = $data['ean'] ?? null;
        $cest = $data['cest'] ?? null;
        $cfop = $data['cfop'] ?? null;
        $preco = (float)($data['preco'] ?? 0);
        $descricao = $data['descricao'] ?? null;
        $unidade = $data['unidade'] ?? null;
        $empresaId = isset($data['empresa_id']) ? (int)$data['empresa_id'] : null;
        if ($ncm !== null && !Br::isNCM($ncm)) return new Response(422, ['error' => ['code' => 'invalid_ncm', 'message' => 'NCM inválido']]);
        if ($ean !== null && !Br::isEAN($ean)) return new Response(422, ['error' => ['code' => 'invalid_ean', 'message' => 'EAN inválido']]);
        if ($cfop !== null && !Br::isCFOP($cfop)) return new Response(422, ['error' => ['code' => 'invalid_cfop', 'message' => 'CFOP inválido']]);
        // Validar empresa quando informada
        if ($empresaId !== null) {
            try { $chk = $pdo->prepare('SELECT id FROM empresas WHERE id = ? LIMIT 1'); $chk->execute([$empresaId]); if (!$chk->fetch()) return new Response(422, ['error' => ['code' => 'empresa_not_found', 'message' => 'Empresa inválida']]); } catch(\Throwable $e) { /* tabela pode não existir em schemas antigos */ }
        }
        // Inserir com suporte a descricao/unidade quando colunas existirem
        try {
            $stmt = $pdo->prepare('INSERT INTO products (codigo, ean, ncm, cest, cfop, preco, descricao, unidade, empresa_id) VALUES (?,?,?,?,?,?,?,?,?)');
            $stmt->execute([$codigo, $ean, $ncm, $cest, $cfop, $preco, $descricao, $unidade, $empresaId]);
        } catch (\Throwable $e) {
            // Fallback para esquemas antigos sem as colunas
            try {
                $stmt = $pdo->prepare('INSERT INTO products (codigo, ean, ncm, cest, cfop, preco, descricao, unidade) VALUES (?,?,?,?,?,?,?,?)');
                $stmt->execute([$codigo, $ean, $ncm, $cest, $cfop, $preco, $descricao, $unidade]);
            } catch(\Throwable $e2) {
                $stmt = $pdo->prepare('INSERT INTO products (codigo, ean, ncm, cest, cfop, preco) VALUES (?,?,?,?,?,?)');
                $stmt->execute([$codigo, $ean, $ncm, $cest, $cfop, $preco]);
            }
        }
        $newId = (int)$pdo->lastInsertId();
        AuditLogger::logCreate('products', $newId, [
            'codigo' => $codigo,
            'ean' => $ean,
            'ncm' => $ncm,
            'cest' => $cest,
            'cfop' => $cfop,
            'preco' => $preco,
            'descricao' => $descricao,
            'unidade' => $unidade,
            'empresa_id' => $empresaId
        ]);
        return Response::json(['ok' => true, 'id' => $newId], 201);
    }

    public function update(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $id = (int)($params[0] ?? 0);
        if ($id <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        $data = $r->body;
        $codigo = $data['codigo'] ?? null;
        $ncm = $data['ncm'] ?? null;
        $ean = $data['ean'] ?? null;
        $cest = $data['cest'] ?? null;
        $cfop = $data['cfop'] ?? null;
        $preco = isset($data['preco']) ? (float)$data['preco'] : null;
        $descricao = $data['descricao'] ?? null;
        $unidade = $data['unidade'] ?? null;
        $empresaId = isset($data['empresa_id']) ? (int)$data['empresa_id'] : null;
        if ($ncm !== null && !Br::isNCM($ncm)) return new Response(422, ['error' => ['code' => 'invalid_ncm', 'message' => 'NCM inválido']]);
        if ($ean !== null && !Br::isEAN($ean)) return new Response(422, ['error' => ['code' => 'invalid_ean', 'message' => 'EAN inválido']]);
        if ($cfop !== null && !Br::isCFOP($cfop)) return new Response(422, ['error' => ['code' => 'invalid_cfop', 'message' => 'CFOP inválido']]);
        if ($empresaId !== null) {
            try { $chk = $pdo->prepare('SELECT id FROM empresas WHERE id = ? LIMIT 1'); $chk->execute([$empresaId]); if (!$chk->fetch()) return new Response(422, ['error' => ['code' => 'empresa_not_found', 'message' => 'Empresa inválida']]); } catch(\Throwable $e) {}
        }
        $beforeStmt = $pdo->prepare('SELECT id, codigo, ean, ncm, cest, cfop, preco FROM products WHERE id = ? LIMIT 1');
        $beforeStmt->execute([$id]);
        $before = $beforeStmt->fetch();
        try {
            $stmt = $pdo->prepare('UPDATE products SET codigo = COALESCE(?, codigo), ean = COALESCE(?, ean), ncm = COALESCE(?, ncm), cest = COALESCE(?, cest), cfop = COALESCE(?, cfop), preco = COALESCE(?, preco), descricao = COALESCE(?, descricao), unidade = COALESCE(?, unidade), empresa_id = COALESCE(?, empresa_id), updated_at = NOW() WHERE id = ?');
            $stmt->execute([$codigo, $ean, $ncm, $cest, $cfop, $preco, $descricao, $unidade, $empresaId, $id]);
        } catch (\Throwable $e) {
            // Fallback para esquemas antigos
            $stmt = $pdo->prepare('UPDATE products SET codigo = COALESCE(?, codigo), ean = COALESCE(?, ean), ncm = COALESCE(?, ncm), cest = COALESCE(?, cest), cfop = COALESCE(?, cfop), preco = COALESCE(?, preco), updated_at = NOW() WHERE id = ?');
            $stmt->execute([$codigo, $ean, $ncm, $cest, $cfop, $preco, $id]);
        }

        $after = $before ?: ['id' => $id];
        if ($codigo !== null) { $after['codigo'] = $codigo; }
        if ($ean !== null) { $after['ean'] = $ean; }
        if ($ncm !== null) { $after['ncm'] = $ncm; }
        if ($cest !== null) { $after['cest'] = $cest; }
        if ($cfop !== null) { $after['cfop'] = $cfop; }
        if ($preco !== null) { $after['preco'] = $preco; }
        if ($descricao !== null) { $after['descricao'] = $descricao; }
        if ($unidade !== null) { $after['unidade'] = $unidade; }
        if ($empresaId !== null) { $after['empresa_id'] = $empresaId; }
        AuditLogger::logUpdate('products', $id, $before ?: [], $after, ['endpoint' => 'produtos.update']);

        return Response::json(['ok' => true]);
    }

    public function patch(Request $r): Response {
        // Mesmo comportamento do update, atualiza apenas campos enviados
        return $this->update($r);
    }

    public function delete(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $id = (int)($params[0] ?? 0);
        if ($id <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        $beforeStmt = $pdo->prepare('SELECT id, codigo, ean, ncm, cest, cfop, preco FROM products WHERE id = ? LIMIT 1');
        $beforeStmt->execute([$id]);
        $before = $beforeStmt->fetch();

        $stmt = $pdo->prepare('DELETE FROM products WHERE id = ?');
        $stmt->execute([$id]);
        AuditLogger::logDelete('products', $id, $before ?: [], ['endpoint' => 'produtos.delete']);
        return Response::json(['ok' => true]);
    }

    // Metadados do produto (JSON único por produto)
    public function saveMeta(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $productId = (int)($params[0] ?? 0);
        if ($productId <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        // Verificar existência do produto
        $chk = $pdo->prepare('SELECT id FROM products WHERE id = ? LIMIT 1');
        $chk->execute([$productId]);
        if (!$chk->fetch()) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Produto não encontrado']]);
        $meta = $r->body ?? [];
        $json = json_encode($meta, JSON_UNESCAPED_UNICODE);
        // Upsert por chave única product_id
        $sql = 'INSERT INTO product_meta (product_id, meta_json) VALUES (?, CAST(? AS JSON))
                ON DUPLICATE KEY UPDATE meta_json = CAST(VALUES(meta_json) AS JSON), updated_at = NOW()';
        try {
            $stmt = $pdo->prepare($sql);
            $stmt->execute([$productId, $json]);
            AuditLogger::logUpdate('product_meta', $productId, [], $meta, ['endpoint' => 'produtos.meta']);
            return Response::json(['ok' => true]);
        } catch (\Throwable $e) {
            // Fallback: armazenar como TEXT caso JSON não seja suportado por dialeto
            try {
                $stmt = $pdo->prepare('INSERT INTO product_meta (product_id, meta_json) VALUES (?, ?)
                    ON DUPLICATE KEY UPDATE meta_json = VALUES(meta_json), updated_at = NOW()');
                $stmt->execute([$productId, $json]);
                AuditLogger::logUpdate('product_meta', $productId, [], $meta, ['endpoint' => 'produtos.meta_text']);
                return Response::json(['ok' => true]);
            } catch (\Throwable $e2) {
                return new Response(500, ['error' => ['code' => 'meta_save_failed', 'message' => $e2->getMessage()]]);
            }
        }
    }

    // Upload de mídias: até 5 imagens + link YouTube
    public function uploadMedia(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $productId = (int)($params[0] ?? 0);
        if ($productId <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        $chk = $pdo->prepare('SELECT id FROM products WHERE id = ? LIMIT 1');
        $chk->execute([$productId]);
        if (!$chk->fetch()) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Produto não encontrado']]);

        $saved = [];
        $uploadDir = __DIR__ . '/../../../public/uploads/products/' . $productId;
        if (!is_dir($uploadDir)) @mkdir($uploadDir, 0775, true);
        $thumbDir = $uploadDir . '/thumbs';
        if (!is_dir($thumbDir)) @mkdir($thumbDir, 0775, true);

        // Imagens (array imagens[]) — tolerante a erros (não aborta o upload inteiro)
        $errorsList = [];
        if (!empty($r->files['imagens'])) {
            $f = $r->files['imagens'];
            $names = $f['name'] ?? []; $tmps = $f['tmp_name'] ?? []; $sizes = $f['size'] ?? []; $errors = $f['error'] ?? [];
            $count = is_array($names) ? count($names) : 1;
            for ($i=0; $i<$count; $i++) {
                $err = is_array($errors) ? ($errors[$i] ?? UPLOAD_ERR_OK) : ($errors ?: UPLOAD_ERR_OK);
                if ($err !== UPLOAD_ERR_OK) { $errorsList[] = ['index'=>$i,'code'=>'upload_error','message'=>'Falha no upload']; continue; }
                $name = is_array($names) ? (string)($names[$i] ?? '') : (string)$names;
                $tmp = is_array($tmps) ? (string)($tmps[$i] ?? '') : (string)$tmps;
                $size = is_array($sizes) ? (int)($sizes[$i] ?? 0) : (int)$sizes;
                $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
                if ($size > 5*1024*1024) { $errorsList[] = ['index'=>$i,'code'=>'file_too_large','message'=>'Imagem excede 5MB']; continue; }
                if (!in_array($ext, ['jpg','jpeg','png','webp'], true)) { $errorsList[] = ['index'=>$i,'code'=>'invalid_file_ext','message'=>'Formato de imagem não suportado']; continue; }
                $destName = uniqid("prod_{$productId}_img_") . '.' . $ext;
                $dest = $uploadDir . '/' . $destName;
                if (!@move_uploaded_file($tmp, $dest)) { $errorsList[] = ['index'=>$i,'code'=>'upload_failed','message'=>'Falha ao salvar imagem']; continue; }
                // Padronização: filename sempre relativo iniciando por products/{id}/...
                $relative = 'products/' . $productId . '/' . $destName;
                $url = '/uploads/' . $relative;
                // Geração de thumbnail 80x80 (lazy-safe)
                $thumbPath = $thumbDir . '/' . $destName;
                try { self::createThumbnail($dest, $thumbPath, 80, 80); } catch (\Throwable $e) { /* ignore, fallback visual no frontend */ }
                $ins = $pdo->prepare('INSERT INTO product_media (product_id, position, type, url, filename, mime, size) VALUES (?,?,?,?,?,?,?)');
                $mime = match($ext){ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', default => 'application/octet-stream' };
                $ins->execute([$productId, $i, 'image', $url, $relative, $mime, $size]);
                $saved[] = ['type' => 'image', 'url' => $url, 'filename' => $relative];
            }
        }

        // YouTube URL
        $youtubeUrl = (string)($r->body['youtube_url'] ?? '');
        if ($youtubeUrl !== '') {
            $pdo->prepare('INSERT INTO product_media (product_id, position, type, url) VALUES (?,?,?,?)')
                ->execute([$productId, 99, 'youtube', $youtubeUrl]);
            $saved[] = ['type' => 'youtube', 'url' => $youtubeUrl];
        }

        AuditLogger::logUpdate('product_media', $productId, [], ['saved' => $saved, 'errors' => $errorsList], ['endpoint' => 'produtos.media']);
        return Response::json(['ok' => true, 'saved' => $saved, 'errors' => $errorsList]);
    }

    // Listar mídias do produto (para exibir foto/thumbnail)
    public function listMedia(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $productId = (int)($params[0] ?? 0);
        if ($productId <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        $chk = $pdo->prepare('SELECT id FROM products WHERE id = ? LIMIT 1');
        $chk->execute([$productId]);
        if (!$chk->fetch()) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Produto não encontrado']]);
        $stmt = $pdo->prepare('SELECT id, position, type, url, filename, mime, size FROM product_media WHERE product_id = ? ORDER BY position ASC, id ASC LIMIT 10');
        $stmt->execute([$productId]);
        $rows = $stmt->fetchAll();
        // Enriquecimento: expor thumb_url e gerar thumbnail se faltar
        foreach ($rows as &$row) {
            if (($row['type'] ?? '') === 'image') {
                $relative = '';
                if (!empty($row['filename'])) {
                    $relative = (string)$row['filename'];
                } elseif (!empty($row['url'])) {
                    $relative = ltrim((string)preg_replace('#^/uploads/#','', (string)$row['url']), '/');
                }
                if ($relative !== '' && str_starts_with($relative, 'products/')) {
                    $parts = explode('/', $relative);
                    // products/{id}/{name}
                    if (count($parts) >= 3) {
                        $name = end($parts);
                        $pidRel = $parts[1];
                        $thumbRelative = 'products/' . $pidRel . '/thumbs/' . $name;
                        $origAbs = __DIR__ . '/../../../public/uploads/' . $relative;
                        $thumbAbs = __DIR__ . '/../../../public/uploads/' . $thumbRelative;
                        if (@is_file($origAbs) && !@is_file($thumbAbs)) {
                            // Tentar gerar thumbnail sob demanda; se falhar o frontend usa fallback
                            try { @mkdir(dirname($thumbAbs), 0775, true); self::createThumbnail($origAbs, $thumbAbs, 80, 80); } catch (\Throwable $e) { /* noop */ }
                        }
                        if (@is_file($thumbAbs)) { $row['thumb_url'] = '/uploads/' . $thumbRelative; }
                    }
                }
            }
        }
        unset($row);
        return Response::json(['data' => $rows]);
    }

    // Utilitário simples de criação de thumbnail com GD (fallback em caso de ausência)
    private static function createThumbnail(string $src, string $dest, int $maxW, int $maxH): void {
        if (!@is_file($src)) return;
        $info = @getimagesize($src);
        if (!$info) { @copy($src, $dest); return; }
        [$w, $h] = [$info[0], $info[1]];
        $type = $info[2] ?? 0; // IMAGETYPE_*
        $load = null; $save = null; $ext = '';
        switch ($type) {
            case IMAGETYPE_JPEG: $load = 'imagecreatefromjpeg'; $save = 'imagejpeg'; $ext='jpg'; break;
            case IMAGETYPE_PNG: $load = 'imagecreatefrompng'; $save = 'imagepng'; $ext='png'; break;
            case IMAGETYPE_WEBP: $load = 'imagecreatefromwebp'; $save = 'imagewebp'; $ext='webp'; break;
            default: @copy($src, $dest); return;
        }
        if (!function_exists($load) || !function_exists($save)) { @copy($src, $dest); return; }
        $srcImg = @$load($src);
        if (!$srcImg) { @copy($src, $dest); return; }
        $ratio = min($maxW / max(1,$w), $maxH / max(1,$h));
        $newW = max(1, (int)floor($w * $ratio));
        $newH = max(1, (int)floor($h * $ratio));
        $thumb = imagecreatetruecolor($newW, $newH);
        // Preservar transparência
        if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_WEBP) {
            imagealphablending($thumb, false);
            imagesavealpha($thumb, true);
            $transparent = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
            imagefilledrectangle($thumb, 0, 0, $newW, $newH, $transparent);
        }
        imagecopyresampled($thumb, $srcImg, 0, 0, 0, 0, $newW, $newH, $w, $h);
        @mkdir(dirname($dest), 0775, true);
        // Qualidade default sensata
        if ($save === 'imagejpeg') { @$save($thumb, $dest, 85); }
        else if ($save === 'imagepng') { @$save($thumb, $dest, 6); }
        else { @$save($thumb, $dest); }
        imagedestroy($thumb);
        imagedestroy($srcImg);
    }

    // Views
    public function viewIndex(Request $r): Response {
        return View::renderWithLayout('produtos/index', ['title' => 'Produtos']);
    }

    public function viewShow(Request $r): Response {
        $params = $r->get('params', []);
        $id = (int)($params[0] ?? 0);
        return View::renderWithLayout('produtos/show', ['title' => 'Detalhes do Produto', 'id' => $id]);
    }

    public function viewCreate(Request $request): Response {
        return View::renderWithLayout('produtos/create', ['title' => 'Criar Produto']);
    }

    public function viewEdit(Request $request): Response {
        $params = $request->get('params', []);
        $id = (int)($params[0] ?? 0);
        // Reutiliza a mesma tela de cadastro, em modo edição
        return View::renderWithLayout('produtos/create', ['title' => 'Editar Produto', 'id' => $id, 'mode' => 'edit']);
    }

    // Obter metadados do produto (meta_json) para edição
    public function getMeta(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $params = $r->get('params', []);
        $productId = (int)($params[0] ?? 0);
        if ($productId <= 0) return new Response(422, ['error' => ['code' => 'invalid_id', 'message' => 'ID inválido']]);
        // Verificar existência do produto
        $chk = $pdo->prepare('SELECT id FROM products WHERE id = ? LIMIT 1');
        $chk->execute([$productId]);
        if (!$chk->fetch()) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Produto não encontrado']]);
        // Buscar meta_json (quando tabela existir)
        try {
            $stmt = $pdo->prepare('SELECT meta_json FROM product_meta WHERE product_id = ? LIMIT 1');
            $stmt->execute([$productId]);
            $row = $stmt->fetch();
            $meta = [];
            if ($row && isset($row['meta_json']) && $row['meta_json'] !== null) {
                $decoded = json_decode((string)$row['meta_json'], true);
                if (is_array($decoded)) $meta = $decoded;
            }
            return Response::json(['data' => $meta]);
        } catch (\Throwable $e) {
            return new Response(500, ['error' => ['code' => 'meta_fetch_failed', 'message' => $e->getMessage()]]);
        }
    }

    // Seed de 5 produtos completos (campos fiscais, meta e mídias) para testes
    public function seedExamples(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $created = [];
        // Verificar existência de product_meta
        $hasMeta = false;
        try {
            $chk = $pdo->prepare("SELECT COUNT(*) AS c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'product_meta'");
            $chk->execute();
            $hasMeta = ((int)($chk->fetch()['c'] ?? 0)) > 0;
        } catch (\Throwable $eChk) { $hasMeta = false; }

        $samples = [
            [
                'codigo' => 'SKU-EX-001', 'ean' => '7891234567890', 'ncm' => '12345678', 'cest' => '1234567', 'cfop' => '5102', 'preco' => 49.90,
                'meta' => [
                    'nome' => 'Camiseta Dry Fit', 'marca' => 'FitWear', 'categoria' => 'Roupas', 'descricao' => 'Camiseta esportiva dry fit, respirável.',
                    'cor' => 'Preta', 'tamanho' => 'M', 'unidade' => 'UN',
                    'peso_liquido' => '0.180', 'peso_bruto' => '0.200', 'largura_cm' => '28', 'altura_cm' => '2', 'comprimento_cm' => '35',
                    'fabricante' => 'FitWear Indústria', 'origem' => '0',
                    'pis_cst' => '01', 'pis_aliquota' => '1.65', 'cofins_cst' => '01', 'cofins_aliquota' => '7.60',
                    'icms_origem' => '0', 'icms_cst' => '00', 'icms_aliquota' => '18', 'csosn' => '102',
                    'ean_pack' => '17891234567897', 'estoque_atual' => 50, 'estoque_minimo' => 5
                ]
            ],
            [
                'codigo' => 'SKU-EX-002', 'ean' => '7891234567891', 'ncm' => '19059010', 'cest' => '2800100', 'cfop' => '5102', 'preco' => 9.99,
                'meta' => [
                    'nome' => 'Barra de Cereal', 'marca' => 'NutriBar', 'categoria' => 'Alimentos', 'descricao' => 'Barra integral com castanhas e mel.',
                    'sabor' => 'Castanhas e Mel', 'unidade' => 'UN', 'peso_liquido' => '0.030', 'peso_bruto' => '0.035',
                    'largura_cm' => '4', 'altura_cm' => '2', 'comprimento_cm' => '12', 'fabricante' => 'NutriBar Alimentos', 'origem' => '0',
                    'pis_cst' => '01', 'pis_aliquota' => '1.65', 'cofins_cst' => '01', 'cofins_aliquota' => '7.60',
                    'icms_origem' => '0', 'icms_cst' => '00', 'icms_aliquota' => '12', 'csosn' => '102',
                    'ean_pack' => '17891234567898', 'estoque_atual' => 200, 'estoque_minimo' => 20
                ]
            ],
            [
                'codigo' => 'SKU-EX-003', 'ean' => '7891234567892', 'ncm' => '33049910', 'cest' => '1301800', 'cfop' => '5405', 'preco' => 29.50,
                'meta' => [
                    'nome' => 'Creme Hidratante', 'marca' => 'DermaPlus', 'categoria' => 'Cosméticos', 'descricao' => 'Hidratante facial para pele seca.',
                    'unidade' => 'UN', 'volume_ml' => '200', 'peso_liquido' => '0.220', 'peso_bruto' => '0.250',
                    'largura_cm' => '7', 'altura_cm' => '15', 'comprimento_cm' => '4', 'fabricante' => 'DermaPlus Laboratórios', 'origem' => '0',
                    'pis_cst' => '01', 'pis_aliquota' => '1.65', 'cofins_cst' => '01', 'cofins_aliquota' => '7.60',
                    'icms_origem' => '0', 'icms_cst' => '00', 'icms_aliquota' => '18', 'csosn' => '102',
                    'ean_pack' => '17891234567899', 'estoque_atual' => 120, 'estoque_minimo' => 10
                ]
            ],
            [
                'codigo' => 'SKU-EX-004', 'ean' => '7891234567893', 'ncm' => '85044010', 'cest' => '2101200', 'cfop' => '5102', 'preco' => 79.00,
                'meta' => [
                    'nome' => 'Carregador USB-C 65W', 'marca' => 'VoltX', 'categoria' => 'Eletrônicos', 'descricao' => 'Carregador rápido com suporte a PD.',
                    'cor' => 'Preto', 'unidade' => 'UN', 'peso_liquido' => '0.180', 'peso_bruto' => '0.220',
                    'largura_cm' => '7', 'altura_cm' => '4', 'comprimento_cm' => '7', 'fabricante' => 'VoltX Eletrônicos', 'origem' => '0',
                    'pis_cst' => '01', 'pis_aliquota' => '1.65', 'cofins_cst' => '01', 'cofins_aliquota' => '7.60',
                    'icms_origem' => '0', 'icms_cst' => '00', 'icms_aliquota' => '18', 'csosn' => '102',
                    'ean_pack' => '17891234567900', 'estoque_atual' => 45, 'estoque_minimo' => 5
                ]
            ],
            [
                'codigo' => 'SKU-EX-005', 'ean' => '7891234567894', 'ncm' => '42029200', 'cest' => '2000200', 'cfop' => '5102', 'preco' => 119.90,
                'meta' => [
                    'nome' => 'Mochila Antifurto', 'marca' => 'UrbanPack', 'categoria' => 'Acessórios', 'descricao' => 'Mochila com zíper oculto e USB externo.',
                    'cor' => 'Cinza', 'unidade' => 'UN', 'peso_liquido' => '0.900', 'peso_bruto' => '1.100', 'largura_cm' => '30', 'altura_cm' => '45', 'comprimento_cm' => '18',
                    'fabricante' => 'UrbanPack Indústria', 'origem' => '0',
                    'pis_cst' => '01', 'pis_aliquota' => '1.65', 'cofins_cst' => '01', 'cofins_aliquota' => '7.60',
                    'icms_origem' => '0', 'icms_cst' => '00', 'icms_aliquota' => '12', 'csosn' => '102',
                    'ean_pack' => '17891234567901', 'estoque_atual' => 80, 'estoque_minimo' => 8
                ]
            ],
        ];

        try {
            foreach ($samples as $s) {
                // Criar produto ou reutilizar existente pelo código (evitar duplicatas)
                $check = $pdo->prepare('SELECT id FROM products WHERE codigo = ? LIMIT 1');
                $check->execute([$s['codigo']]);
                $row = $check->fetch();
                if ($row && isset($row['id'])) {
                    $pid = (int)$row['id'];
                } else {
                    $ins = $pdo->prepare('INSERT INTO products (codigo, ean, ncm, cest, cfop, preco) VALUES (?,?,?,?,?,?)');
                    $ins->execute([$s['codigo'], $s['ean'], $s['ncm'], $s['cest'], $s['cfop'], $s['preco']]);
                    $pid = (int)$pdo->lastInsertId();
                    AuditLogger::logCreate('products', $pid, $s);
                }
                // Metadados
                if ($hasMeta) {
                    $metaJson = json_encode($s['meta'], JSON_UNESCAPED_UNICODE);
                    try {
                        $sql = 'INSERT INTO product_meta (product_id, meta_json) VALUES (?, CAST(? AS JSON))
                                ON DUPLICATE KEY UPDATE meta_json = CAST(VALUES(meta_json) AS JSON), updated_at = NOW()';
                        $stm = $pdo->prepare($sql);
                        $stm->execute([$pid, $metaJson]);
                    } catch (\Throwable $e) {
                        $sql2 = 'INSERT INTO product_meta (product_id, meta_json) VALUES (?, ?)
                                ON DUPLICATE KEY UPDATE meta_json = VALUES(meta_json), updated_at = NOW()';
                        $stm2 = $pdo->prepare($sql2);
                        $stm2->execute([$pid, $metaJson]);
                    }
                    AuditLogger::logUpdate('product_meta', $pid, [], $s['meta'], ['endpoint' => 'seed.meta']);
                }
                // Mídias (copiar imagens de assets como exemplo)
                $uploadDir = __DIR__ . '/../../../public/uploads/products/' . $pid;
                if (!is_dir($uploadDir)) { @mkdir($uploadDir, 0775, true); }
                $assetsDir = __DIR__ . '/../../../public/assets/';
                $img1Src = $assetsDir . 'logo-light.png';
                $img2Src = $assetsDir . 'logo-dark.png';
                $img1Dest = $uploadDir . '/example1.png';
                $img2Dest = $uploadDir . '/example2.png';
                if (@copy($img1Src, $img1Dest)) {
                    $size1 = @filesize($img1Dest) ?: 0;
                    $pm1 = $pdo->prepare('INSERT INTO product_media (product_id, type, filename, mime, size, position) VALUES (?,?,?,?,?,?)');
                    $pm1->execute([$pid, 'image', 'products/' . $pid . '/example1.png', 'image/png', $size1, 0]);
                    // gerar thumbnail para manter consistência
                    try { @mkdir($uploadDir . '/thumbs', 0775, true); self::createThumbnail($img1Dest, $uploadDir . '/thumbs/example1.png', 80, 80); } catch (\Throwable $e) { }
                }
                if (@copy($img2Src, $img2Dest)) {
                    $size2 = @filesize($img2Dest) ?: 0;
                    $pm2 = $pdo->prepare('INSERT INTO product_media (product_id, type, filename, mime, size, position) VALUES (?,?,?,?,?,?)');
                    $pm2->execute([$pid, 'image', 'products/' . $pid . '/example2.png', 'image/png', $size2, 1]);
                    try { @mkdir($uploadDir . '/thumbs', 0775, true); self::createThumbnail($img2Dest, $uploadDir . '/thumbs/example2.png', 80, 80); } catch (\Throwable $e) { }
                }
                // Vídeo de exemplo (URL YouTube)
                try {
                    $pmv = $pdo->prepare('INSERT INTO product_media (product_id, type, url, position) VALUES (?,?,?,?)');
                    $pmv->execute([$pid, 'video', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 2]);
                } catch (\Throwable $e) { /* opcional: pode não existir coluna url, ignorar */ }

                $created[] = $pid;
            }
            return Response::json(['ok' => true, 'created_ids' => $created]);
        } catch (\Throwable $e) {
            return new Response(500, ['error' => ['code' => 'seed_failed', 'message' => $e->getMessage()]]);
        }
    }
}