<?php
namespace Modules\Clientes\Controllers;

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

class ClientsController {
    // Expose server-side geocoding with proper User-Agent headers
    public function geocode(Request $req): Response {
        // Accept either free-text q or structured params
        $q = $req->get('q');
        if ($q) {
            $url = 'https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' . urlencode($q);
            $ctx = stream_context_create(['http' => [
                'method' => 'GET',
                'timeout' => 8,
                'header' => "Accept: application/json\r\nUser-Agent: gestoreasy/clients-module"
            ]]);
            $res = @file_get_contents($url, false, $ctx);
            if ($res === false) { return new Response(200, ['ok' => false, 'error' => 'request_failed']); }
            $json = json_decode($res, true);
            if (!is_array($json) || count($json) === 0) { return new Response(200, ['ok' => false, 'error' => 'not_found']); }
            $it = $json[0];
            return new Response(200, ['ok' => true, 'lat' => floatval($it['lat']), 'lon' => floatval($it['lon'])]);
        }
        $addr = [
            'logradouro' => $req->get('logradouro') ?? '',
            'numero' => $req->get('numero') ?? '',
            'bairro' => $req->get('bairro') ?? '',
            'cidade' => $req->get('cidade') ?? '',
            'estado' => $req->get('estado') ?? '',
            'cep' => $req->get('cep') ?? ''
        ];
        $geo = $this->geocodeAddress($addr);
        if (!$geo) { return new Response(200, ['ok' => false, 'error' => 'not_found']); }
        return new Response(200, ['ok' => true, 'lat' => $geo['lat'], 'lon' => $geo['lon']]);
    }
    private function geocodeAddress(array $addr): ?array {
        $parts = [];
        if (!empty($addr['logradouro'])) $parts[] = $addr['logradouro'];
        if (!empty($addr['numero'])) $parts[] = (string)$addr['numero'];
        if (!empty($addr['bairro'])) $parts[] = $addr['bairro'];
        if (!empty($addr['cidade'])) $parts[] = $addr['cidade'];
        if (!empty($addr['estado'])) $parts[] = $addr['estado'];
        if (!empty($addr['cep'])) $parts[] = preg_replace('/\D/', '', (string)$addr['cep']);
        if (!$parts) return null;
        $q = implode(', ', $parts);
        $url = 'https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' . urlencode($q);
        $ctx = stream_context_create(['http' => [
            'method' => 'GET',
            'timeout' => 7,
            'header' => "Accept: application/json\r\nUser-Agent: gestoreasy/clients-module"
        ]]);
        $res = @file_get_contents($url, false, $ctx);
        if ($res === false) return null;
        $json = json_decode($res, true);
        if (!is_array($json) || count($json) === 0) return null;
        $item = $json[0];
        $lat = isset($item['lat']) ? (float)$item['lat'] : null;
        $lon = isset($item['lon']) ? (float)$item['lon'] : null;
        if ($lat === null || $lon === null) return null;
        return ['lat' => $lat, 'lon' => $lon];
    }
    public function cepLookup(Request $r): Response {
        $params = $r->get('params', []);
        $cep = preg_replace('/\D/', '', (string)($params[0] ?? ''));
        if (!Br::isCEP($cep)) return new Response(422, ['error' => ['code' => 'invalid_cep', 'message' => 'CEP inválido']]);

        $fetchJson = function (string $url, int $timeout = 6): ?array {
            $ctx = stream_context_create(['http' => ['method' => 'GET', 'timeout' => $timeout]]);
            $res = @file_get_contents($url, false, $ctx);
            if ($res === false) return null;
            $json = json_decode($res, true);
            return is_array($json) ? $json : null;
        };

        $data = $fetchJson("https://viacep.com.br/ws/{$cep}/json/");
        if (!$data || isset($data['erro'])) {
            return new Response(404, ['error' => ['code' => 'cep_not_found', 'message' => 'CEP não encontrado']]);
        }

        $norm = [
            'cep' => preg_replace('/\D/', '', ($data['cep'] ?? $cep)),
            'logradouro' => $data['logradouro'] ?? null,
            'complemento' => $data['complemento'] ?? null,
            'bairro' => $data['bairro'] ?? null,
            'cidade' => $data['localidade'] ?? null,
            'estado' => $data['uf'] ?? null,
        ];

        return Response::json(['data' => $norm]);
    }
    public function cnpjLookup(Request $r): Response {
        $params = $r->get('params', []);
        $cnpj = preg_replace('/\D/', '', (string)($params[0] ?? ''));
        if (!Br::isCNPJ($cnpj)) return new Response(422, ['error' => ['code' => 'invalid_cnpj', 'message' => 'CNPJ inválido']]);

        $pdo = ConnectionManager::forTenant();
        $ttl = (int)\Gestoreasy\Core\Config\Config::get('CNPJ_CACHE_TTL_SECONDS', 86400);
        $minRefresh = (int)\Gestoreasy\Core\Config\Config::get('CNPJ_REFRESH_MIN_SECONDS', 60);
        $refresh = filter_var(($r->query['refresh'] ?? 'false'), FILTER_VALIDATE_BOOLEAN);
        if (!$refresh) {
            $stmt = $pdo->prepare('SELECT data_json, fetched_at FROM cnpj_cache WHERE cnpj = ? LIMIT 1');
            $stmt->execute([$cnpj]);
            $row = $stmt->fetch();
            if ($row) {
                $age = time() - strtotime($row['fetched_at']);
                if ($age < $ttl) {
                    $json = json_decode($row['data_json'], true);
                    if (is_array($json)) return Response::json(['data' => $json, 'cached' => true]);
                }
            }
        }
        else {
            // Limita refresh frequente para evitar abuso
            $stmt = $pdo->prepare('SELECT fetched_at FROM cnpj_cache WHERE cnpj = ? LIMIT 1');
            $stmt->execute([$cnpj]);
            $row = $stmt->fetch();
            if ($row) {
                $age = time() - strtotime($row['fetched_at']);
                if ($age < $minRefresh) {
                    $retryIn = $minRefresh - $age;
                    return new Response(429, ['error' => ['code' => 'refresh_too_soon', 'message' => 'Refresh muito frequente'], 'retry_in_seconds' => $retryIn]);
                }
            }
        }

        $fetchJson = function (string $url, int $timeout = 6): ?array {
            $ctx = stream_context_create(['http' => ['method' => 'GET', 'timeout' => $timeout]]);
            $res = @file_get_contents($url, false, $ctx);
            if ($res === false) return null;
            $json = json_decode($res, true);
            return is_array($json) ? $json : null;
        };

        // Tenta BrasilAPI
        $data = $fetchJson("https://brasilapi.com.br/api/cnpj/v1/{$cnpj}");
        $source = 'brasilapi';
        if (!$data) {
            // Fallback Receitaws (não oficial, sujeito a rate limits)
            $data = $fetchJson("https://www.receitaws.com.br/v1/cnpj/{$cnpj}");
            $source = $data ? 'receitaws' : $source;
        }
        if (!$data) return new Response(502, ['error' => ['code' => 'cnpj_service_unavailable', 'message' => 'Falha ao consultar serviço de CNPJ']]);

        // Normaliza resposta
        $norm = [
            'cnpj' => $cnpj,
            'razao_social' => $data['razao_social'] ?? ($data['nome'] ?? null),
            'nome_fantasia' => $data['nome_fantasia'] ?? ($data['fantasia'] ?? null),
            'telefone' => $data['telefone'] ?? null,
            'email' => $data['email'] ?? null,
            'situacao' => $data['descricao_situacao_cadastral'] ?? ($data['situacao'] ?? null),
            'data_situacao' => $data['data_situacao_cadastral'] ?? ($data['data_situacao'] ?? null),
            'capital_social' => $data['capital_social'] ?? null,
            'atividade_principal' => [
                'codigo' => (string)($data['cnae_fiscal'] ?? ($data['atividade_principal'][0]['code'] ?? '')),
                'descricao' => $data['cnae_fiscal_descricao'] ?? ($data['atividade_principal'][0]['text'] ?? null),
            ],
            'atividades_secundarias' => array_map(function ($it) {
                return [
                    'codigo' => (string)($it['code'] ?? $it['codigo'] ?? ''),
                    'descricao' => $it['text'] ?? $it['descricao'] ?? null,
                ];
            }, (array)($data['cnaes_secundarios'] ?? ($data['atividades_secundarias'] ?? []))),
            'endereco' => [
                'logradouro' => $data['descricao_tipo_logradouro'] ?? $data['logradouro'] ?? null,
                'numero' => $data['numero'] ?? null,
                'complemento' => $data['complemento'] ?? null,
                'bairro' => $data['bairro'] ?? null,
                'cep' => $data['cep'] ?? null,
                'municipio' => $data['municipio'] ?? ($data['municipio'] ?? null),
                'uf' => $data['uf'] ?? $data['uf'] ?? null,
            ],
            'qsa' => array_map(function ($q) {
                return [
                    'nome' => $q['nome_socio'] ?? $q['nome'] ?? null,
                    'qualificacao' => $q['qualificacao_socio'] ?? $q['qual'] ?? null,
                ];
            }, (array)($data['qsa'] ?? [])),
            'source' => $source,
        ];

        // Persiste cache
        $stmt = $pdo->prepare('INSERT INTO cnpj_cache (cnpj, data_json, fetched_at) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE data_json = VALUES(data_json), fetched_at = VALUES(fetched_at)');
        $stmt->execute([$cnpj, json_encode($norm)]);

        return Response::json(['data' => $norm, 'cached' => false]);
    }

    public function list(Request $r): Response {
        $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 = [];
        if ($q !== '') {
            $where = 'WHERE nome LIKE ? OR cpf LIKE ? OR cnpj LIKE ?';
            $like = "%$q%";
            $params = [$like, $like, $like];
        }
        $offset = ($page - 1) * $per;
        $sql = "SELECT id, nome, nome_fantasia, razao_social, tipo, cpf, cnpj, telefone_fixo, telefone_celular, email, contato_nome, site_url, referencias, limite_credito, bloqueado, inativo, status_motivo FROM clients $where ORDER BY id DESC LIMIT $per OFFSET $offset";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $rows = $stmt->fetchAll();
        $countSql = "SELECT COUNT(*) AS c FROM clients $where";
        $countStmt = $pdo->prepare($countSql);
        $countStmt->execute($params);
        $total = (int)$countStmt->fetch()['c'];
        $totalPages = (int)ceil($total / $per);
        AuditLogger::logRead('clients', null, ['endpoint' => 'clientes.list', 'q' => $q, 'page' => $page, 'per_page' => $per]);
        return Response::json(['data' => $rows, 'page' => $page, 'per_page' => $per, 'total' => $total, 'total_pages' => $totalPages]);
    }

    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, nome, nome_fantasia, razao_social, tipo, cpf, cnpj, ie, im, telefone_fixo, telefone_celular, referencias, limite_credito, imagem_url, arquivo_url, email, contato_nome, site_url, observacoes, bloqueado, inativo, status_motivo, created_at, updated_at FROM clients WHERE id = ? LIMIT 1');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        if (!$row) return new Response(404, ['error' => ['code' => 'not_found', 'message' => 'Cliente não encontrado']]);
        // Incluir endereços (principal, cobrança, entrega) se existirem
        try {
            $addrStmt = $pdo->prepare('SELECT tipo, logradouro, numero, complemento, bairro, cidade, estado, cep, latitude, longitude FROM client_addresses WHERE client_id = ? AND tipo IN ("principal","cobranca","entrega")');
            $addrStmt->execute([$id]);
            $addrs = $addrStmt->fetchAll();
            foreach ($addrs as $a) {
                if (($a['tipo'] ?? '') === 'principal') { $row['address'] = $a; }
                else if (($a['tipo'] ?? '') === 'cobranca') { $row['billing_address'] = $a; }
                else if (($a['tipo'] ?? '') === 'entrega') { $row['shipping_address'] = $a; }
            }
        } catch (\Throwable $e) { /* tabela pode não ter sido migrada completamente ainda */ }
        AuditLogger::logRead('clients', $id, ['endpoint' => 'clientes.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;
        $tipo = $data['tipo'] ?? 'PJ';
        $nomeFantasia = $data['nome_fantasia'] ?? '';
        $razaoSocial = $data['razao_social'] ?? '';
        $cpf = $data['cpf'] ?? null;
        $cnpj = $data['cnpj'] ?? null;
        $ie = isset($data['ie']) ? trim((string)$data['ie']) : '';
        $im = isset($data['im']) ? trim((string)$data['im']) : '';
        // Basic validations
        if (!$razaoSocial || !is_string($razaoSocial)) return new Response(422, ['error' => ['code' => 'invalid_razao_social', 'message' => 'Razão social é obrigatória']]);
        if (!$nomeFantasia || !is_string($nomeFantasia)) return new Response(422, ['error' => ['code' => 'invalid_nome_fantasia', 'message' => 'Nome fantasia é obrigatório']]);
        if ($tipo === 'PF') {
            if (!$cpf || !Br::isCPF($cpf)) return new Response(422, ['error' => ['code' => 'invalid_cpf', 'message' => 'CPF inválido']]);
        } else if ($tipo === 'PJ') {
            if (!$cnpj || !Br::isCNPJ($cnpj)) return new Response(422, ['error' => ['code' => 'invalid_cnpj', 'message' => 'CNPJ inválido']]);
        }
        // IE/IM obrigatórias (permitir "ISENTO" para IE)
        $estadoUf = strtoupper((string)($data['estado'] ?? ''));
        $ieUpper = strtoupper($ie);
        if ($ieUpper === '') return new Response(422, ['error' => ['code' => 'invalid_ie', 'message' => 'Inscrição Estadual é obrigatória']]);
        if ($ieUpper !== 'ISENTO') {
            $ieDigits = preg_replace('/[^0-9A-Z]/', '', $ieUpper);
            $sizes = ['SP'=>12,'RJ'=>8,'MG'=>13,'PR'=>10,'RS'=>10,'SC'=>9,'BA'=>8,'ES'=>9,'PE'=>14,'CE'=>9,'GO'=>9,'MT'=>11,'MS'=>9,'PA'=>8,'AL'=>9,'AM'=>9,'DF'=>13,'RN'=>9,'RO'=>14,'TO'=>9,'MA'=>9,'PB'=>9,'PI'=>9,'SE'=>9];
            $expected = $sizes[$estadoUf] ?? 9;
            if (strlen($ieDigits) !== $expected) return new Response(422, ['error' => ['code' => 'invalid_ie_format', 'message' => 'Inscrição Estadual inválida para a UF']]);
        }
        // IM opcional: valida formato somente se informado
        if ($im !== '' && !preg_match('/^[0-9A-Za-z\.\-\/]{5,20}$/', $im)) return new Response(422, ['error' => ['code' => 'invalid_im_format', 'message' => 'Inscrição Municipal inválida']]);
        // Telefones
        $telFixo = $data['telefone_fixo'] ?? null;
        $telCelular = $data['telefone_celular'] ?? null;
        if ($telFixo && !Br::isTelefoneFixo($telFixo)) return new Response(422, ['error' => ['code' => 'invalid_tel_fixo', 'message' => 'Telefone fixo inválido']]);
        if ($telCelular && !Br::isTelefoneCelular($telCelular)) return new Response(422, ['error' => ['code' => 'invalid_tel_celular', 'message' => 'Telefone celular inválido']]);

        $referencias = $data['referencias'] ?? null;
        $limiteCreditoRaw = $data['limite_credito'] ?? null;
        $limiteCredito = null;
        if ($limiteCreditoRaw !== null && $limiteCreditoRaw !== '') {
            // Converter formato BR (1.234,56) para decimal
            $norm = str_replace(['.', ','], ['', '.'], $limiteCreditoRaw);
            if (!is_numeric($norm)) return new Response(422, ['error' => ['code' => 'invalid_limite_credito', 'message' => 'Limite de crédito inválido']]);
            $limiteCredito = number_format((float)$norm, 2, '.', '');
        }

        $stmt = $pdo->prepare("INSERT INTO clients (nome, nome_fantasia, razao_social, tipo, cpf, cnpj, ie, im, telefone_fixo, telefone_celular, referencias, limite_credito) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
        $stmt->execute([$nomeFantasia, $nomeFantasia, $razaoSocial, $tipo, $cpf, $cnpj, $ieUpper, $im, $telFixo, $telCelular, $referencias, $limiteCredito]);
        $newId = (int)$pdo->lastInsertId();
        AuditLogger::logCreate('clients', $newId, ['nome_fantasia' => $nomeFantasia, 'razao_social' => $razaoSocial, 'tipo' => $tipo, 'cpf' => $cpf, 'cnpj' => $cnpj]);

        // Uploads
        $uploads = ['imagem' => ['max' => 2 * 1024 * 1024, 'ext' => ['jpg','jpeg','png']], 'arquivo' => ['max' => 5 * 1024 * 1024, 'ext' => ['pdf','doc','docx','xls','xlsx']]];
        $uploadDir = __DIR__ . '/../../../public/uploads/clients';
        if (!is_dir($uploadDir)) @mkdir($uploadDir, 0775, true);
        foreach ($uploads as $field => $rules) {
            if (!empty($r->files[$field]) && ($r->files[$field]['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
                $f = $r->files[$field];
                $size = (int)($f['size'] ?? 0);
                $name = (string)($f['name'] ?? '');
                $tmp = (string)($f['tmp_name'] ?? '');
                $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
                if ($size > $rules['max']) return new Response(413, ['error' => ['code' => 'file_too_large', 'message' => "Arquivo {$field} excede o limite"]]);
                if (!in_array($ext, $rules['ext'], true)) return new Response(422, ['error' => ['code' => 'invalid_file_ext', 'message' => "Extensão não suportada em {$field}"]]);
                $destName = uniqid("client_{$newId}_{$field}_") . '.' . $ext;
                $dest = $uploadDir . '/' . $destName;
                if (!@move_uploaded_file($tmp, $dest)) return new Response(500, ['error' => ['code' => 'upload_failed', 'message' => "Falha ao salvar {$field}"]]);
                $url = '/uploads/clients/' . $destName;
                $col = $field === 'imagem' ? 'imagem_url' : 'arquivo_url';
                $pdo->prepare("UPDATE clients SET {$col} = ? WHERE id = ?")->execute([$url, $newId]);
            }
        }

        // Address handling - principal
        $logradouro = $data['logradouro'] ?? null;
        $numero = $data['numero'] ?? null;
        $complemento = $data['complemento'] ?? null;
        $bairro = $data['bairro'] ?? null;
        $cidade = $data['cidade'] ?? null;
        $estado = $data['estado'] ?? null;
        $cep = $data['cep'] ?? null;

        if ($cep && !Br::isCEP($cep)) return new Response(422, ['error' => ['code' => 'invalid_cep', 'message' => 'CEP inválido']]);
        if ($estado && !preg_match('/^[A-Z]{2}$/', $estado)) return new Response(422, ['error' => ['code' => 'invalid_estado', 'message' => 'Estado deve ser UF de 2 letras']]);

        $hasAddr = $logradouro || $numero || $complemento || $bairro || $cidade || $estado || $cep;
        if ($hasAddr) {
            $addrStmt = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
            $addrStmt->execute([$newId, 'principal', $logradouro, $numero, $complemento, $bairro, $cidade, $estado, $cep]);
            $addrId = (int)$pdo->lastInsertId();
            // Usa coordenadas fornecidas se presentes; caso contrário, geocodifica
            $latIn = isset($data['principal_latitude']) ? $data['principal_latitude'] : ($data['latitude'] ?? null);
            $lonIn = isset($data['principal_longitude']) ? $data['principal_longitude'] : ($data['longitude'] ?? null);
            if ($latIn !== null && $lonIn !== null && $latIn !== '' && $lonIn !== '') {
                $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                $up->execute([floatval($latIn), floatval($lonIn), $addrId]);
            } else {
                $geo = $this->geocodeAddress(['logradouro'=>$logradouro,'numero'=>$numero,'bairro'=>$bairro,'cidade'=>$cidade,'estado'=>$estado,'cep'=>$cep]);
                if ($geo) {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([$geo['lat'], $geo['lon'], $addrId]);
                }
            }
        }

        // Address handling - cobrança (opcional)
        $billEnabled = filter_var(($data['billing_enabled'] ?? 'false'), FILTER_VALIDATE_BOOLEAN);
        if ($billEnabled) {
            $b_logradouro = $data['billing_logradouro'] ?? null;
            $b_numero = $data['billing_numero'] ?? null;
            $b_complemento = $data['billing_complemento'] ?? null;
            $b_bairro = $data['billing_bairro'] ?? null;
            $b_cidade = $data['billing_cidade'] ?? null;
            $b_estado = $data['billing_estado'] ?? null;
            $b_cep = $data['billing_cep'] ?? null;
            if (!$b_logradouro || !$b_numero || !$b_bairro || !$b_cidade || !$b_estado || !$b_cep) {
                return new Response(422, ['error' => ['code' => 'billing_required', 'message' => 'Endereço de cobrança incompleto']]);
            }
            if ($b_cep && !Br::isCEP($b_cep)) return new Response(422, ['error' => ['code' => 'invalid_billing_cep', 'message' => 'CEP de cobrança inválido']]);
            if ($b_estado && !preg_match('/^[A-Z]{2}$/', $b_estado)) return new Response(422, ['error' => ['code' => 'invalid_billing_estado', 'message' => 'UF de cobrança inválida']]);
            $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
            $ins->execute([$newId, 'cobranca', $b_logradouro, $b_numero, $b_complemento, $b_bairro, $b_cidade, $b_estado, $b_cep]);
            $addrId = (int)$pdo->lastInsertId();
            $b_latIn = $data['billing_latitude'] ?? null;
            $b_lonIn = $data['billing_longitude'] ?? null;
            if ($b_latIn !== null && $b_lonIn !== null && $b_latIn !== '' && $b_lonIn !== '') {
                $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([floatval($b_latIn), floatval($b_lonIn), $addrId]);
            } else {
                $geo = $this->geocodeAddress(['logradouro'=>$b_logradouro,'numero'=>$b_numero,'bairro'=>$b_bairro,'cidade'=>$b_cidade,'estado'=>$b_estado,'cep'=>$b_cep]);
                if ($geo) { $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([$geo['lat'], $geo['lon'], $addrId]); }
            }
        }

        // Address handling - entrega (opcional)
        $shipEnabled = filter_var(($data['shipping_enabled'] ?? 'false'), FILTER_VALIDATE_BOOLEAN);
        if ($shipEnabled) {
            $s_logradouro = $data['shipping_logradouro'] ?? null;
            $s_numero = $data['shipping_numero'] ?? null;
            $s_complemento = $data['shipping_complemento'] ?? null;
            $s_bairro = $data['shipping_bairro'] ?? null;
            $s_cidade = $data['shipping_cidade'] ?? null;
            $s_estado = $data['shipping_estado'] ?? null;
            $s_cep = $data['shipping_cep'] ?? null;
            if (!$s_logradouro || !$s_numero || !$s_bairro || !$s_cidade || !$s_estado || !$s_cep) {
                return new Response(422, ['error' => ['code' => 'shipping_required', 'message' => 'Endereço de entrega incompleto']]);
            }
            if ($s_cep && !Br::isCEP($s_cep)) return new Response(422, ['error' => ['code' => 'invalid_shipping_cep', 'message' => 'CEP de entrega inválido']]);
            if ($s_estado && !preg_match('/^[A-Z]{2}$/', $s_estado)) return new Response(422, ['error' => ['code' => 'invalid_shipping_estado', 'message' => 'UF de entrega inválida']]);
            $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
            $ins->execute([$newId, 'entrega', $s_logradouro, $s_numero, $s_complemento, $s_bairro, $s_cidade, $s_estado, $s_cep]);
            $addrId = (int)$pdo->lastInsertId();
            $s_latIn = $data['shipping_latitude'] ?? null;
            $s_lonIn = $data['shipping_longitude'] ?? null;
            if ($s_latIn !== null && $s_lonIn !== null && $s_latIn !== '' && $s_lonIn !== '') {
                $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([floatval($s_latIn), floatval($s_lonIn), $addrId]);
            } else {
                $geo = $this->geocodeAddress(['logradouro'=>$s_logradouro,'numero'=>$s_numero,'bairro'=>$s_bairro,'cidade'=>$s_cidade,'estado'=>$s_estado,'cep'=>$s_cep]);
                if ($geo) { $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([$geo['lat'], $geo['lon'], $addrId]); }
            }
        }
        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;
        $tipo = $data['tipo'] ?? null;
        $nomeFantasia = $data['nome_fantasia'] ?? null;
        $razaoSocial = $data['razao_social'] ?? null;
        $cpf = $data['cpf'] ?? null;
        $cnpj = $data['cnpj'] ?? null;
        if ($tipo === 'PF') {
            if ($cpf !== null && !Br::isCPF($cpf)) return new Response(422, ['error' => ['code' => 'invalid_cpf', 'message' => 'CPF inválido']]);
        } else if ($tipo === 'PJ') {
            if ($cnpj !== null && !Br::isCNPJ($cnpj)) return new Response(422, ['error' => ['code' => 'invalid_cnpj', 'message' => 'CNPJ inválido']]);
        }
        $beforeStmt = $pdo->prepare('SELECT id, nome_fantasia, razao_social, tipo, cpf, cnpj, ie, im, telefone_fixo, telefone_celular, referencias, limite_credito, imagem_url, arquivo_url FROM clients WHERE id = ? LIMIT 1');
        $beforeStmt->execute([$id]);
        $before = $beforeStmt->fetch();

        // Telefones
        $telFixo = $data['telefone_fixo'] ?? null;
        $telCelular = $data['telefone_celular'] ?? null;
        if ($telFixo !== null && $telFixo !== '' && !Br::isTelefoneFixo($telFixo)) return new Response(422, ['error' => ['code' => 'invalid_tel_fixo', 'message' => 'Telefone fixo inválido']]);
        if ($telCelular !== null && $telCelular !== '' && !Br::isTelefoneCelular($telCelular)) return new Response(422, ['error' => ['code' => 'invalid_tel_celular', 'message' => 'Telefone celular inválido']]);
        $referencias = $data['referencias'] ?? null;
        $limiteCreditoRaw = $data['limite_credito'] ?? null;
        $limiteCredito = null;
        if ($limiteCreditoRaw !== null) {
            if ($limiteCreditoRaw === '') { $limiteCredito = null; }
            else {
                $norm = str_replace(['.', ','], ['', '.'], $limiteCreditoRaw);
                if (!is_numeric($norm)) return new Response(422, ['error' => ['code' => 'invalid_limite_credito', 'message' => 'Limite de crédito inválido']]);
                $limiteCredito = number_format((float)$norm, 2, '.', '');
            }
        }

        // Campos adicionais de gestão
        $email = isset($data['email']) ? trim((string)$data['email']) : null;
        if ($email !== null && $email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) return new Response(422, ['error' => ['code' => 'invalid_email', 'message' => 'Email inválido']]);
        $contatoNome = $data['contato_nome'] ?? null;
        $siteUrl = $data['site_url'] ?? null;
        $observacoes = $data['observacoes'] ?? null;
        $bloqueado = isset($data['bloqueado']) ? (int)filter_var($data['bloqueado'], FILTER_VALIDATE_BOOLEAN) : null;
        $inativo = isset($data['inativo']) ? (int)filter_var($data['inativo'], FILTER_VALIDATE_BOOLEAN) : null;
        $statusMotivo = $data['status_motivo'] ?? null;
        if (($bloqueado === 1 || $inativo === 1) && (!$statusMotivo || trim((string)$statusMotivo) === '')) {
            return new Response(422, ['error' => ['code' => 'status_motivo_required', 'message' => 'Informe o motivo para bloqueio/inativação']]);
        }

        $stmt = $pdo->prepare('UPDATE clients SET nome = COALESCE(?, nome), nome_fantasia = COALESCE(?, nome_fantasia), razao_social = COALESCE(?, razao_social), tipo = COALESCE(?, tipo), cpf = COALESCE(?, cpf), cnpj = COALESCE(?, cnpj), ie = COALESCE(?, ie), im = COALESCE(?, im), telefone_fixo = COALESCE(?, telefone_fixo), telefone_celular = COALESCE(?, telefone_celular), referencias = COALESCE(?, referencias), limite_credito = COALESCE(?, limite_credito), email = COALESCE(?, email), contato_nome = COALESCE(?, contato_nome), site_url = COALESCE(?, site_url), observacoes = COALESCE(?, observacoes), bloqueado = COALESCE(?, bloqueado), inativo = COALESCE(?, inativo), status_motivo = COALESCE(?, status_motivo), updated_at = NOW() WHERE id = ?');
        $stmt->execute([$nomeFantasia, $nomeFantasia, $razaoSocial, $tipo, $cpf, $cnpj, isset($ie) ? strtoupper(trim((string)$ie)) : null, isset($im) ? trim((string)$im) : null, $telFixo, $telCelular, $referencias, $limiteCredito, $email, $contatoNome, $siteUrl, $observacoes, $bloqueado, $inativo, $statusMotivo, $id]);

        $after = $before ?: ['id' => $id];
        if ($nomeFantasia !== null) { $after['nome_fantasia'] = $nomeFantasia; }
        if ($razaoSocial !== null) { $after['razao_social'] = $razaoSocial; }
        if ($tipo !== null) { $after['tipo'] = $tipo; }
        if ($cpf !== null) { $after['cpf'] = $cpf; }
        if ($cnpj !== null) { $after['cnpj'] = $cnpj; }
        if ($telFixo !== null) { $after['telefone_fixo'] = $telFixo; }
        if ($telCelular !== null) { $after['telefone_celular'] = $telCelular; }
        if ($ie !== null) { $after['ie'] = strtoupper(trim((string)$ie)); }
        if ($im !== null) { $after['im'] = trim((string)$im); }
        if ($referencias !== null) { $after['referencias'] = $referencias; }
        if ($limiteCredito !== null) { $after['limite_credito'] = $limiteCredito; }
        AuditLogger::logUpdate('clients', $id, $before ?: [], $after, ['endpoint' => 'clientes.update']);

        // Uploads - handle file uploads for update
        $uploads = ['imagem' => ['max' => 2 * 1024 * 1024, 'ext' => ['jpg','jpeg','png']], 'arquivo' => ['max' => 5 * 1024 * 1024, 'ext' => ['pdf','doc','docx','xls','xlsx']]];
        $uploadDir = __DIR__ . '/../../../public/uploads/clients';
        if (!is_dir($uploadDir)) @mkdir($uploadDir, 0775, true);
        foreach ($uploads as $field => $rules) {
            if (!empty($r->files[$field]) && ($r->files[$field]['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
                $f = $r->files[$field];
                $size = (int)($f['size'] ?? 0);
                $name = (string)($f['name'] ?? '');
                $tmp = (string)($f['tmp_name'] ?? '');
                $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
                if ($size > $rules['max']) return new Response(413, ['error' => ['code' => 'file_too_large', 'message' => "Arquivo {$field} excede o limite"]]);
                if (!in_array($ext, $rules['ext'], true)) return new Response(422, ['error' => ['code' => 'invalid_file_ext', 'message' => "Extensão não suportada em {$field}"]]);
                
                // Remove old file if exists
                $col = $field === 'imagem' ? 'imagem_url' : 'arquivo_url';
                $oldFileStmt = $pdo->prepare("SELECT {$col} FROM clients WHERE id = ? LIMIT 1");
                $oldFileStmt->execute([$id]);
                $oldFileRow = $oldFileStmt->fetch();
                if ($oldFileRow && $oldFileRow[$col]) {
                    $oldFilePath = __DIR__ . '/../../../public' . $oldFileRow[$col];
                    if (file_exists($oldFilePath)) @unlink($oldFilePath);
                }
                
                $destName = uniqid("client_{$id}_{$field}_") . '.' . $ext;
                $dest = $uploadDir . '/' . $destName;
                if (!@move_uploaded_file($tmp, $dest)) return new Response(500, ['error' => ['code' => 'upload_failed', 'message' => "Falha ao salvar {$field}"]]);
                $url = '/uploads/clients/' . $destName;
                $pdo->prepare("UPDATE clients SET {$col} = ? WHERE id = ?")->execute([$url, $id]);
            }
        }

        // Address upsert - principal
        $logradouro = $data['logradouro'] ?? null;
        $numero = $data['numero'] ?? null;
        $complemento = $data['complemento'] ?? null;
        $bairro = $data['bairro'] ?? null;
        $cidade = $data['cidade'] ?? null;
        $estado = $data['estado'] ?? null;
        $cep = $data['cep'] ?? null;

        if ($cep !== null && $cep !== '' && !Br::isCEP($cep)) return new Response(422, ['error' => ['code' => 'invalid_cep', 'message' => 'CEP inválido']]);
        if ($estado !== null && $estado !== '' && !preg_match('/^[A-Z]{2}$/', $estado)) return new Response(422, ['error' => ['code' => 'invalid_estado', 'message' => 'Estado deve ser UF de 2 letras']]);

        $hasAddrChange = ($logradouro !== null) || ($numero !== null) || ($complemento !== null) || ($bairro !== null) || ($cidade !== null) || ($estado !== null) || ($cep !== null);
        if ($hasAddrChange) {
            $findStmt = $pdo->prepare('SELECT id FROM client_addresses WHERE client_id = ? AND tipo = ? LIMIT 1');
            $findStmt->execute([$id, 'principal']);
            $addrRow = $findStmt->fetch();
            if ($addrRow) {
                $upd = $pdo->prepare('UPDATE client_addresses SET logradouro = COALESCE(?, logradouro), numero = COALESCE(?, numero), complemento = COALESCE(?, complemento), bairro = COALESCE(?, bairro), cidade = COALESCE(?, cidade), estado = COALESCE(?, estado), cep = COALESCE(?, cep) WHERE id = ?');
                $upd->execute([$logradouro, $numero, $complemento, $bairro, $cidade, $estado, $cep, $addrRow['id']]);
                // Preferir coordenadas fornecidas; caso ausentes, geocodificar
                $latIn = isset($data['principal_latitude']) ? $data['principal_latitude'] : ($data['latitude'] ?? null);
                $lonIn = isset($data['principal_longitude']) ? $data['principal_longitude'] : ($data['longitude'] ?? null);
                if ($latIn !== null && $lonIn !== null && $latIn !== '' && $lonIn !== '') {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([floatval($latIn), floatval($lonIn), $addrRow['id']]);
                } else {
                    $geo = $this->geocodeAddress([
                        'logradouro' => $logradouro ?? '',
                        'numero' => $numero ?? '',
                        'bairro' => $bairro ?? '',
                        'cidade' => $cidade ?? '',
                        'estado' => $estado ?? '',
                        'cep' => $cep ?? ''
                    ]);
                    if ($geo) {
                        $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                        $up->execute([$geo['lat'], $geo['lon'], $addrRow['id']]);
                    }
                }
            } else {
                $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
                $ins->execute([$id, 'principal', $logradouro, $numero, $complemento, $bairro, $cidade, $estado, $cep]);
                $addrId = (int)$pdo->lastInsertId();
                $latIn = isset($data['principal_latitude']) ? $data['principal_latitude'] : ($data['latitude'] ?? null);
                $lonIn = isset($data['principal_longitude']) ? $data['principal_longitude'] : ($data['longitude'] ?? null);
                if ($latIn !== null && $lonIn !== null && $latIn !== '' && $lonIn !== '') {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([floatval($latIn), floatval($lonIn), $addrId]);
                } else {
                    $geo = $this->geocodeAddress(['logradouro'=>$logradouro,'numero'=>$numero,'bairro'=>$bairro,'cidade'=>$cidade,'estado'=>$estado,'cep'=>$cep]);
                    if ($geo) {
                        $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                        $up->execute([$geo['lat'], $geo['lon'], $addrId]);
                    }
                }
            }
        }

        // Address upsert - billing (cobrança)
        $b_logradouro = $data['billing_logradouro'] ?? null;
        $b_numero = $data['billing_numero'] ?? null;
        $b_complemento = $data['billing_complemento'] ?? null;
        $b_bairro = $data['billing_bairro'] ?? null;
        $b_cidade = $data['billing_cidade'] ?? null;
        $b_estado = $data['billing_estado'] ?? null;
        $b_cep = $data['billing_cep'] ?? null;

        if ($b_cep !== null && $b_cep !== '' && !Br::isCEP($b_cep)) return new Response(422, ['error' => ['code' => 'invalid_billing_cep', 'message' => 'CEP de cobrança inválido']]);
        if ($b_estado !== null && $b_estado !== '' && !preg_match('/^[A-Z]{2}$/', $b_estado)) return new Response(422, ['error' => ['code' => 'invalid_billing_estado', 'message' => 'Estado de cobrança deve ser UF de 2 letras']]);

        $hasBillingAddrChange = ($b_logradouro !== null) || ($b_numero !== null) || ($b_complemento !== null) || ($b_bairro !== null) || ($b_cidade !== null) || ($b_estado !== null) || ($b_cep !== null);
        if ($hasBillingAddrChange) {
            $findStmt = $pdo->prepare('SELECT id FROM client_addresses WHERE client_id = ? AND tipo = ? LIMIT 1');
            $findStmt->execute([$id, 'cobranca']);
            $addrRow = $findStmt->fetch();
            if ($addrRow) {
                $upd = $pdo->prepare('UPDATE client_addresses SET logradouro = COALESCE(?, logradouro), numero = COALESCE(?, numero), complemento = COALESCE(?, complemento), bairro = COALESCE(?, bairro), cidade = COALESCE(?, cidade), estado = COALESCE(?, estado), cep = COALESCE(?, cep) WHERE id = ?');
                $upd->execute([$b_logradouro, $b_numero, $b_complemento, $b_bairro, $b_cidade, $b_estado, $b_cep, $addrRow['id']]);
                $b_latIn = $data['billing_latitude'] ?? null;
                $b_lonIn = $data['billing_longitude'] ?? null;
                if ($b_latIn !== null && $b_lonIn !== null && $b_latIn !== '' && $b_lonIn !== '') {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([floatval($b_latIn), floatval($b_lonIn), $addrRow['id']]);
                } else {
                    $geo = $this->geocodeAddress([
                        'logradouro' => $b_logradouro ?? '',
                        'numero' => $b_numero ?? '',
                        'bairro' => $b_bairro ?? '',
                        'cidade' => $b_cidade ?? '',
                        'estado' => $b_estado ?? '',
                        'cep' => $b_cep ?? ''
                    ]);
                    if ($geo) {
                        $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                        $up->execute([$geo['lat'], $geo['lon'], $addrRow['id']]);
                    }
                }
            } else {
                $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
                $ins->execute([$id, 'cobranca', $b_logradouro, $b_numero, $b_complemento, $b_bairro, $b_cidade, $b_estado, $b_cep]);
                $addrId = (int)$pdo->lastInsertId();
                $b_latIn = $data['billing_latitude'] ?? null;
                $b_lonIn = $data['billing_longitude'] ?? null;
                if ($b_latIn !== null && $b_lonIn !== null && $b_latIn !== '' && $b_lonIn !== '') {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([floatval($b_latIn), floatval($b_lonIn), $addrId]);
                } else {
                    $geo = $this->geocodeAddress(['logradouro'=>$b_logradouro,'numero'=>$b_numero,'bairro'=>$b_bairro,'cidade'=>$b_cidade,'estado'=>$b_estado,'cep'=>$b_cep]);
                    if ($geo) {
                        $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                        $up->execute([$geo['lat'], $geo['lon'], $addrId]);
                    }
                }
            }
        }

        // Address upsert - shipping (entrega)
        $s_logradouro = $data['shipping_logradouro'] ?? null;
        $s_numero = $data['shipping_numero'] ?? null;
        $s_complemento = $data['shipping_complemento'] ?? null;
        $s_bairro = $data['shipping_bairro'] ?? null;
        $s_cidade = $data['shipping_cidade'] ?? null;
        $s_estado = $data['shipping_estado'] ?? null;
        $s_cep = $data['shipping_cep'] ?? null;

        if ($s_cep !== null && $s_cep !== '' && !Br::isCEP($s_cep)) return new Response(422, ['error' => ['code' => 'invalid_shipping_cep', 'message' => 'CEP de entrega inválido']]);
        if ($s_estado !== null && $s_estado !== '' && !preg_match('/^[A-Z]{2}$/', $s_estado)) return new Response(422, ['error' => ['code' => 'invalid_shipping_estado', 'message' => 'Estado de entrega deve ser UF de 2 letras']]);

        $hasShippingAddrChange = ($s_logradouro !== null) || ($s_numero !== null) || ($s_complemento !== null) || ($s_bairro !== null) || ($s_cidade !== null) || ($s_estado !== null) || ($s_cep !== null);
        if ($hasShippingAddrChange) {
            $findStmt = $pdo->prepare('SELECT id FROM client_addresses WHERE client_id = ? AND tipo = ? LIMIT 1');
            $findStmt->execute([$id, 'entrega']);
            $addrRow = $findStmt->fetch();
            if ($addrRow) {
                $upd = $pdo->prepare('UPDATE client_addresses SET logradouro = COALESCE(?, logradouro), numero = COALESCE(?, numero), complemento = COALESCE(?, complemento), bairro = COALESCE(?, bairro), cidade = COALESCE(?, cidade), estado = COALESCE(?, estado), cep = COALESCE(?, cep) WHERE id = ?');
                $upd->execute([$s_logradouro, $s_numero, $s_complemento, $s_bairro, $s_cidade, $s_estado, $s_cep, $addrRow['id']]);
                $s_latIn = $data['shipping_latitude'] ?? null;
                $s_lonIn = $data['shipping_longitude'] ?? null;
                if ($s_latIn !== null && $s_lonIn !== null && $s_latIn !== '' && $s_lonIn !== '') {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([floatval($s_latIn), floatval($s_lonIn), $addrRow['id']]);
                } else {
                    $geo = $this->geocodeAddress([
                        'logradouro' => $s_logradouro ?? '',
                        'numero' => $s_numero ?? '',
                        'bairro' => $s_bairro ?? '',
                        'cidade' => $s_cidade ?? '',
                        'estado' => $s_estado ?? '',
                        'cep' => $s_cep ?? ''
                    ]);
                    if ($geo) {
                        $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                        $up->execute([$geo['lat'], $geo['lon'], $addrRow['id']]);
                    }
                }
            } else {
                $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
                $ins->execute([$id, 'entrega', $s_logradouro, $s_numero, $s_complemento, $s_bairro, $s_cidade, $s_estado, $s_cep]);
                $addrId = (int)$pdo->lastInsertId();
                $s_latIn = $data['shipping_latitude'] ?? null;
                $s_lonIn = $data['shipping_longitude'] ?? null;
                if ($s_latIn !== null && $s_lonIn !== null && $s_latIn !== '' && $s_lonIn !== '') {
                    $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                    $up->execute([floatval($s_latIn), floatval($s_lonIn), $addrId]);
                } else {
                    $geo = $this->geocodeAddress(['logradouro'=>$s_logradouro,'numero'=>$s_numero,'bairro'=>$s_bairro,'cidade'=>$s_cidade,'estado'=>$s_estado,'cep'=>$s_cep]);
                    if ($geo) {
                        $up = $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?');
                        $up->execute([$geo['lat'], $geo['lon'], $addrId]);
                    }
                }
            }
        }

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

    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, nome, tipo, cpf, cnpj FROM clients WHERE id = ? LIMIT 1');
        $beforeStmt->execute([$id]);
        $before = $beforeStmt->fetch();

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

    // Views
    public function viewIndex(): Response {
        return View::renderWithLayout('clientes/index', ['title' => 'Clientes']);
    }

    public function viewShow(): Response {
        return View::renderWithLayout('clientes/show', ['title' => 'Cliente']);
    }
    
    public function viewCreate(): Response {
        return View::renderWithLayout('clientes/create', ['title' => 'Novo Cliente']);
    }
    
    public function viewEdit(): Response {
        return View::renderWithLayout('clientes/edit', ['title' => 'Editar Cliente']);
    }

    public function viewMap(): Response {
        return View::renderWithLayout('clientes/mapa', ['title' => 'Mapa de Clientes']);
    }
    
    public function withAddress(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $stmt = $pdo->query('SELECT c.id, c.nome_fantasia, c.razao_social, c.tipo, c.cnpj, a.logradouro, a.numero, a.bairro, a.cidade, a.estado, a.cep, a.latitude, a.longitude FROM clients c LEFT JOIN client_addresses a ON a.client_id = c.id AND a.tipo = "principal" ORDER BY c.id DESC LIMIT 500');
        $rows = $stmt->fetchAll();
        return Response::json(['data' => $rows]);
    }

    public function exportCsv(Request $r): Response {
        $pdo = ConnectionManager::forTenant();
        $sep = (string)($r->query['sep'] ?? ';');
        if (!in_array($sep, [';',',','\t'], true)) $sep = ';';
        if ($sep === '\t') $sep = "\t";
        $stmt = $pdo->query('SELECT 
            c.id, c.nome, c.nome_fantasia, c.razao_social, c.tipo, c.cpf, c.cnpj, c.ie, c.im, 
            c.telefone_fixo, c.telefone_celular, c.referencias, c.limite_credito, 
            c.email, c.contato_nome, c.site_url, c.observacoes, c.bloqueado, c.inativo, c.status_motivo, 
            c.imagem_url, c.arquivo_url, c.created_at, c.updated_at,
            ap.logradouro AS principal_logradouro, ap.numero AS principal_numero, ap.complemento AS principal_complemento, ap.bairro AS principal_bairro, ap.cidade AS principal_cidade, ap.estado AS principal_estado, ap.cep AS principal_cep, ap.latitude AS principal_latitude, ap.longitude AS principal_longitude,
            ab.logradouro AS cobranca_logradouro, ab.numero AS cobranca_numero, ab.complemento AS cobranca_complemento, ab.bairro AS cobranca_bairro, ab.cidade AS cobranca_cidade, ab.estado AS cobranca_estado, ab.cep AS cobranca_cep, ab.latitude AS cobranca_latitude, ab.longitude AS cobranca_longitude,
            ae.logradouro AS entrega_logradouro, ae.numero AS entrega_numero, ae.complemento AS entrega_complemento, ae.bairro AS entrega_bairro, ae.cidade AS entrega_cidade, ae.estado AS entrega_estado, ae.cep AS entrega_cep, ae.latitude AS entrega_latitude, ae.longitude AS entrega_longitude
        FROM clients c 
        LEFT JOIN client_addresses ap ON ap.client_id = c.id AND ap.tipo = "principal"
        LEFT JOIN client_addresses ab ON ab.client_id = c.id AND ab.tipo = "cobranca"
        LEFT JOIN client_addresses ae ON ae.client_id = c.id AND ae.tipo = "entrega"
        ORDER BY c.id DESC');
        $rows = $stmt->fetchAll();
        $bom = chr(0xEF) . chr(0xBB) . chr(0xBF);
        $out = fopen('php://temp', 'r+');
        fputs($out, $bom);
        $headers = [
            'ID','Nome','Nome Fantasia','Razão Social','Tipo','CPF','CNPJ','Inscrição Estadual','Inscrição Municipal',
            'Telefone Fixo','Telefone Celular','Referências','Limite de Crédito','E-mail','Contato','Site','Observações','Bloqueado','Inativo','Motivo Status','URL Imagem','URL Arquivo','Criado em','Atualizado em',
            'Endereço Logradouro','Endereço Número','Endereço Complemento','Endereço Bairro','Endereço Cidade','Endereço Estado','Endereço CEP','Endereço Latitude','Endereço Longitude',
            'Cobrança Logradouro','Cobrança Número','Cobrança Complemento','Cobrança Bairro','Cobrança Cidade','Cobrança Estado','Cobrança CEP','Cobrança Latitude','Cobrança Longitude',
            'Entrega Logradouro','Entrega Número','Entrega Complemento','Entrega Bairro','Entrega Cidade','Entrega Estado','Entrega CEP','Entrega Latitude','Entrega Longitude'
        ];
        fputcsv($out, $headers, $sep);
        foreach ($rows as $row) {
            fputcsv($out, [
                $row['id'] ?? null,
                $row['nome'] ?? null,
                $row['nome_fantasia'] ?? null,
                $row['razao_social'] ?? null,
                $row['tipo'] ?? null,
                $row['cpf'] ?? null,
                $row['cnpj'] ?? null,
                $row['ie'] ?? null,
                $row['im'] ?? null,
                $row['telefone_fixo'] ?? null,
                $row['telefone_celular'] ?? null,
                $row['referencias'] ?? null,
                $row['limite_credito'] ?? null,
                $row['email'] ?? null,
                $row['contato_nome'] ?? null,
                $row['site_url'] ?? null,
                $row['observacoes'] ?? null,
                $row['bloqueado'] ?? null,
                $row['inativo'] ?? null,
                $row['status_motivo'] ?? null,
                $row['imagem_url'] ?? null,
                $row['arquivo_url'] ?? null,
                $row['created_at'] ?? null,
                $row['updated_at'] ?? null,
                $row['principal_logradouro'] ?? null,
                $row['principal_numero'] ?? null,
                $row['principal_complemento'] ?? null,
                $row['principal_bairro'] ?? null,
                $row['principal_cidade'] ?? null,
                $row['principal_estado'] ?? null,
                $row['principal_cep'] ?? null,
                $row['principal_latitude'] ?? null,
                $row['principal_longitude'] ?? null,
                $row['cobranca_logradouro'] ?? null,
                $row['cobranca_numero'] ?? null,
                $row['cobranca_complemento'] ?? null,
                $row['cobranca_bairro'] ?? null,
                $row['cobranca_cidade'] ?? null,
                $row['cobranca_estado'] ?? null,
                $row['cobranca_cep'] ?? null,
                $row['cobranca_latitude'] ?? null,
                $row['cobranca_longitude'] ?? null,
                $row['entrega_logradouro'] ?? null,
                $row['entrega_numero'] ?? null,
                $row['entrega_complemento'] ?? null,
                $row['entrega_bairro'] ?? null,
                $row['entrega_cidade'] ?? null,
                $row['entrega_estado'] ?? null,
                $row['entrega_cep'] ?? null,
                $row['entrega_latitude'] ?? null,
                $row['entrega_longitude'] ?? null,
            ], $sep);
        }
        rewind($out);
        $csv = stream_get_contents($out);
        fclose($out);
        return new Response(200, $csv, ['Content-Type' => 'text/csv; charset=utf-8', 'Content-Disposition' => 'attachment; filename="clientes.csv"']);
    }

    public function importCsv(Request $r): Response {
        if (empty($r->files['file']) || ($r->files['file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            return new Response(422, ['error' => ['code' => 'no_file', 'message' => 'Arquivo CSV é obrigatório']]);
        }
        $sep = (string)($r->query['sep'] ?? ';');
        if ($sep === '\t') $sep = "\t";
        $tmp = $r->files['file']['tmp_name'];
        $fh = fopen($tmp, 'r');
        // pular BOM
        $first = fgets($fh);
        if ($first !== false && str_starts_with($first, "\xEF\xBB\xBF")) {
            $first = substr($first, 3);
        }
        // se a primeira linha não são cabeçalhos esperados, ainda assim prosseguir
        $errors = [];
        $imported = 0;
        $lineNum = 1;
        $pdo = ConnectionManager::forTenant();
        while (($cols = fgetcsv($fh, 0, $sep)) !== false) {
            $lineNum++;
            // Suporta modelo completo e modelo básico
            if (count($cols) < 15) { $errors[] = [ 'line' => $lineNum, 'message' => 'Colunas insuficientes' ]; continue; }
            $nf = $cols[0] ?? '';
            $rs = $cols[1] ?? '';
            $tipo = $cols[2] ?? 'PJ';
            $cpf = $cols[3] ?? '';
            $cnpj = $cols[4] ?? '';
            $ie = $cols[5] ?? '';
            $im = $cols[6] ?? '';
            $fixo = $cols[7] ?? '';
            $cel = $cols[8] ?? '';
            $refs = $cols[9] ?? '';
            $lim = $cols[10] ?? '';
            $email = $cols[11] ?? '';
            $contato = $cols[12] ?? '';
            $site = $cols[13] ?? '';
            $obs = $cols[14] ?? '';
            $bloq = $cols[15] ?? '';
            $inat = $cols[16] ?? '';
            $motivo = $cols[17] ?? '';
            // Endereço principal
            $log = $cols[18] ?? ($cols[9] ?? '');
            $num = $cols[19] ?? ($cols[10] ?? '');
            $comp = $cols[20] ?? ($cols[11] ?? '');
            $bair = $cols[21] ?? ($cols[12] ?? '');
            $cid = $cols[22] ?? ($cols[13] ?? '');
            $uf = $cols[23] ?? ($cols[14] ?? '');
            $cep = $cols[24] ?? ($cols[14] ?? '');
            // Endereço cobrança
            $b_log = $cols[25] ?? null; $b_num = $cols[26] ?? null; $b_comp = $cols[27] ?? null; $b_bairro = $cols[28] ?? null; $b_cid = $cols[29] ?? null; $b_uf = $cols[30] ?? null; $b_cep = $cols[31] ?? null;
            // Endereço entrega
            $s_log = $cols[32] ?? null; $s_num = $cols[33] ?? null; $s_comp = $cols[34] ?? null; $s_bairro = $cols[35] ?? null; $s_cid = $cols[36] ?? null; $s_uf = $cols[37] ?? null; $s_cep = $cols[38] ?? null;
            // Se modelo básico (15 colunas), realocar variáveis
            if (count($cols) === 15) {
                [$nf,$rs,$tipo,$cnpj,$fixo,$cel,$refs,$lim,$log,$num,$comp,$bair,$cid,$uf,$cep] = array_slice($cols,0,15);
                $cpf=''; $ie=''; $im=''; $email=''; $contato=''; $site=''; $obs=''; $bloq=''; $inat=''; $motivo='';
                $b_log=$b_num=$b_comp=$b_bairro=$b_cid=$b_uf=$b_cep=null;
                $s_log=$s_num=$s_comp=$s_bairro=$s_cid=$s_uf=$s_cep=null;
            }
            $cnpj = preg_replace('/\D/','',$cnpj);
            $cpf = preg_replace('/\D/','',$cpf);
            $cep = preg_replace('/\D/','',$cep);
            $b_cep = $b_cep ? preg_replace('/\D/','',$b_cep) : null;
            $s_cep = $s_cep ? preg_replace('/\D/','',$s_cep) : null;
            if ($tipo !== 'PJ' && $tipo !== 'PF') { $errors[] = ['line'=>$lineNum,'message'=>'Tipo inválido']; continue; }
            if ($tipo === 'PJ' && !Br::isCNPJ($cnpj)) { $errors[] = ['line'=>$lineNum,'message'=>'CNPJ inválido']; continue; }
            if ($tipo === 'PF' && $cpf !== '' && !Br::isCPF($cpf)) { $errors[] = ['line'=>$lineNum,'message'=>'CPF inválido']; continue; }
            if ($cep && !Br::isCEP($cep)) { $errors[] = ['line'=>$lineNum,'message'=>'CEP inválido']; continue; }
            if ($b_cep && !Br::isCEP($b_cep)) { $errors[] = ['line'=>$lineNum,'message'=>'CEP de cobrança inválido']; continue; }
            if ($s_cep && !Br::isCEP($s_cep)) { $errors[] = ['line'=>$lineNum,'message'=>'CEP de entrega inválido']; continue; }
            $limDec = null;
            if ($lim !== null && $lim !== '') { $norm = str_replace(['.',','],['','.'],$lim); if (!is_numeric($norm)) { $errors[] = ['line'=>$lineNum,'message'=>'Limite inválido']; continue; } $limDec = number_format((float)$norm,2,'.',''); }
            $bloqFlag = filter_var($bloq, FILTER_VALIDATE_BOOLEAN);
            $inatFlag = filter_var($inat, FILTER_VALIDATE_BOOLEAN);
            $stmt = $pdo->prepare("INSERT INTO clients (nome, nome_fantasia, razao_social, tipo, cpf, cnpj, ie, im, telefone_fixo, telefone_celular, referencias, limite_credito, email, contato_nome, site_url, observacoes, bloqueado, inativo, status_motivo) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
            $stmt->execute([
                $nf ?: $rs,
                $nf ?: null,
                $rs ?: null,
                $tipo,
                ($cpf !== '' ? $cpf : null),
                ($cnpj !== '' ? $cnpj : null),
                ($ie !== '' ? strtoupper($ie) : null),
                ($im !== '' ? $im : null),
                ($fixo !== '' ? $fixo : null),
                ($cel !== '' ? $cel : null),
                ($refs !== '' ? $refs : null),
                $limDec,
                ($email !== '' ? $email : null),
                ($contato !== '' ? $contato : null),
                ($site !== '' ? $site : null),
                ($obs !== '' ? $obs : null),
                $bloqFlag ? 1 : 0,
                $inatFlag ? 1 : 0,
                ($motivo !== '' ? $motivo : null)
            ]);
            $cid = (int)$pdo->lastInsertId();
            $addrStmt = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
            $addrStmt->execute([$cid,'principal',$log,$num,$comp,$bair,$cid,$uf,$cep]);
            if ($b_log && $b_num && $b_bairro && $b_cid && $b_uf && $b_cep) {
                $addrStmt->execute([$cid,'cobranca',$b_log,$b_num,$b_comp,$b_bairro,$b_cid,$b_uf,$b_cep]);
            }
            if ($s_log && $s_num && $s_bairro && $s_cid && $s_uf && $s_cep) {
                $addrStmt->execute([$cid,'entrega',$s_log,$s_num,$s_comp,$s_bairro,$s_cid,$s_uf,$s_cep]);
            }
            $imported++;
        }
        fclose($fh);
        return Response::json(['ok' => true, 'imported' => $imported, 'errors' => $errors]);
    }

    public function templateCsv(): Response {
        $sep = ';';
        $bom = chr(0xEF) . chr(0xBB) . chr(0xBF);
        $out = fopen('php://temp', 'r+');
        fputs($out, $bom);
        $headers = [
            'Nome Fantasia','Razão Social','Tipo','CPF','CNPJ','Inscrição Estadual','Inscrição Municipal','Telefone Fixo','Telefone Celular','Referências','Limite de Crédito','E-mail','Contato','Site','Observações','Bloqueado','Inativo','Motivo Status',
            'Endereço Logradouro','Endereço Número','Endereço Complemento','Endereço Bairro','Endereço Cidade','Endereço Estado','Endereço CEP',
            'Cobrança Logradouro','Cobrança Número','Cobrança Complemento','Cobrança Bairro','Cobrança Cidade','Cobrança Estado','Cobrança CEP',
            'Entrega Logradouro','Entrega Número','Entrega Complemento','Entrega Bairro','Entrega Cidade','Entrega Estado','Entrega CEP'
        ];
        fputcsv($out, $headers, $sep);
        // Exemplo PJ completo
        $examplePJ = ['Empresa Exemplo Ltda','Empresa Exemplo Ltda','PJ','','12.345.678/0001-95','ISENTO','','(11) 2345-6789','(11) 98765-4321','Fornecedores A; B','1.500,00','contato@exemplo.com','Maria','https://www.exemplo.com','Observações gerais','false','false','',
            'Av. Paulista','1000','Conj. 101','Bela Vista','São Paulo','SP','01311-300',
            'Rua da Cobrança','200','','Centro','São Paulo','SP','01000-000',
            'Rua da Entrega','300','','Centro','São Paulo','SP','01000-001'];
        fputcsv($out, $examplePJ, $sep);
        // Exemplo PF básico
        $examplePF = ['João da Silva','João da Silva','PF','123.456.789-09','','','','(21) 2345-6789','(21) 98765-4321','', '0,00','joao@exemplo.com','','','', 'false','false','',
            'Rua das Laranjeiras','50','','Laranjeiras','Rio de Janeiro','RJ','22240-003','','','','','','','','','',''];
        fputcsv($out, $examplePF, $sep);
        rewind($out);
        $csv = stream_get_contents($out);
        fclose($out);
        return new Response(200, $csv, ['Content-Type' => 'text/csv; charset=utf-8', 'Content-Disposition' => 'attachment; filename="template_importacao_clientes.csv"']);
    }
}
        // IE/IM
        $ie = $data['ie'] ?? null;
        $im = $data['im'] ?? null;
        $estadoUf = strtoupper((string)($data['estado'] ?? ''));
        if ($ie !== null) {
            $ieTrim = strtoupper(trim((string)$ie));
            if ($ieTrim === '') return new Response(422, ['error' => ['code' => 'invalid_ie', 'message' => 'Inscrição Estadual não pode ser vazia']]);
            if ($ieTrim !== 'ISENTO') {
                $ieDigits = preg_replace('/[^0-9A-Z]/', '', $ieTrim);
                $sizes = ['SP'=>12,'RJ'=>8,'MG'=>13,'PR'=>10,'RS'=>10,'SC'=>9,'BA'=>8,'ES'=>9,'PE'=>14,'CE'=>9,'GO'=>9,'MT'=>11,'MS'=>9,'PA'=>8,'AL'=>9,'AM'=>9,'DF'=>13,'RN'=>9,'RO'=>14,'TO'=>9,'MA'=>9,'PB'=>9,'PI'=>9,'SE'=>9];
                $expected = $sizes[$estadoUf] ?? 9;
                if (strlen($ieDigits) !== $expected) return new Response(422, ['error' => ['code' => 'invalid_ie_format', 'message' => 'Inscrição Estadual inválida para a UF']]);
            }
        }
        if ($im !== null) {
            $imTrim = trim((string)$im);
            if ($imTrim === '') return new Response(422, ['error' => ['code' => 'invalid_im', 'message' => 'Inscrição Municipal não pode ser vazia']]);
            if (!preg_match('/^[0-9A-Za-z\.\-\/]{5,20}$/', $imTrim)) return new Response(422, ['error' => ['code' => 'invalid_im_format', 'message' => 'Inscrição Municipal inválida']]);
        }

        // Address upsert - cobrança
        $billEnabled = isset($data['billing_enabled']) ? filter_var($data['billing_enabled'], FILTER_VALIDATE_BOOLEAN) : null;
        if ($billEnabled) {
            $findStmt = $pdo->prepare('SELECT id FROM client_addresses WHERE client_id = ? AND tipo = ? LIMIT 1');
            $findStmt->execute([$id, 'cobranca']);
            $addrRow = $findStmt->fetch();
            $b_logradouro = $data['billing_logradouro'] ?? null;
            $b_numero = $data['billing_numero'] ?? null;
            $b_complemento = $data['billing_complemento'] ?? null;
            $b_bairro = $data['billing_bairro'] ?? null;
            $b_cidade = $data['billing_cidade'] ?? null;
            $b_estado = $data['billing_estado'] ?? null;
            $b_cep = $data['billing_cep'] ?? null;
            if (!$b_logradouro || !$b_numero || !$b_bairro || !$b_cidade || !$b_estado || !$b_cep) {
                return new Response(422, ['error' => ['code' => 'billing_required', 'message' => 'Endereço de cobrança incompleto']]);
            }
            if ($b_cep && !Br::isCEP($b_cep)) return new Response(422, ['error' => ['code' => 'invalid_billing_cep', 'message' => 'CEP de cobrança inválido']]);
            if ($b_estado && !preg_match('/^[A-Z]{2}$/', $b_estado)) return new Response(422, ['error' => ['code' => 'invalid_billing_estado', 'message' => 'UF de cobrança inválida']]);
            if ($addrRow) {
                $upd = $pdo->prepare('UPDATE client_addresses SET logradouro = ?, numero = ?, complemento = ?, bairro = ?, cidade = ?, estado = ?, cep = ? WHERE id = ?');
                $upd->execute([$b_logradouro, $b_numero, $b_complemento, $b_bairro, $b_cidade, $b_estado, $b_cep, $addrRow['id']]);
                $geo = $this->geocodeAddress(['logradouro'=>$b_logradouro,'numero'=>$b_numero,'bairro'=>$b_bairro,'cidade'=>$b_cidade,'estado'=>$b_estado,'cep'=>$b_cep]);
                if ($geo) { $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([$geo['lat'], $geo['lon'], $addrRow['id']]); }
            } else {
                $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
                $ins->execute([$id, 'cobranca', $b_logradouro, $b_numero, $b_complemento, $b_bairro, $b_cidade, $b_estado, $b_cep]);
                $addrId = (int)$pdo->lastInsertId();
                $geo = $this->geocodeAddress(['logradouro'=>$b_logradouro,'numero'=>$b_numero,'bairro'=>$b_bairro,'cidade'=>$b_cidade,'estado'=>$b_estado,'cep'=>$b_cep]);
                if ($geo) { $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([$geo['lat'], $geo['lon'], $addrId]); }
            }
        }

        // Address upsert - entrega
        $shipEnabled = isset($data['shipping_enabled']) ? filter_var($data['shipping_enabled'], FILTER_VALIDATE_BOOLEAN) : null;
        if ($shipEnabled) {
            $findStmt = $pdo->prepare('SELECT id FROM client_addresses WHERE client_id = ? AND tipo = ? LIMIT 1');
            $findStmt->execute([$id, 'entrega']);
            $addrRow = $findStmt->fetch();
            $s_logradouro = $data['shipping_logradouro'] ?? null;
            $s_numero = $data['shipping_numero'] ?? null;
            $s_complemento = $data['shipping_complemento'] ?? null;
            $s_bairro = $data['shipping_bairro'] ?? null;
            $s_cidade = $data['shipping_cidade'] ?? null;
            $s_estado = $data['shipping_estado'] ?? null;
            $s_cep = $data['shipping_cep'] ?? null;
            if (!$s_logradouro || !$s_numero || !$s_bairro || !$s_cidade || !$s_estado || !$s_cep) {
                return new Response(422, ['error' => ['code' => 'shipping_required', 'message' => 'Endereço de entrega incompleto']]);
            }
            if ($s_cep && !Br::isCEP($s_cep)) return new Response(422, ['error' => ['code' => 'invalid_shipping_cep', 'message' => 'CEP de entrega inválido']]);
            if ($s_estado && !preg_match('/^[A-Z]{2}$/', $s_estado)) return new Response(422, ['error' => ['code' => 'invalid_shipping_estado', 'message' => 'UF de entrega inválida']]);
            if ($addrRow) {
                $upd = $pdo->prepare('UPDATE client_addresses SET logradouro = ?, numero = ?, complemento = ?, bairro = ?, cidade = ?, estado = ?, cep = ? WHERE id = ?');
                $upd->execute([$s_logradouro, $s_numero, $s_complemento, $s_bairro, $s_cidade, $s_estado, $s_cep, $addrRow['id']]);
                $geo = $this->geocodeAddress(['logradouro'=>$s_logradouro,'numero'=>$s_numero,'bairro'=>$s_bairro,'cidade'=>$s_cidade,'estado'=>$s_estado,'cep'=>$s_cep]);
                if ($geo) { $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([$geo['lat'], $geo['lon'], $addrRow['id']]); }
            } else {
                $ins = $pdo->prepare('INSERT INTO client_addresses (client_id, tipo, logradouro, numero, complemento, bairro, cidade, estado, cep) VALUES (?,?,?,?,?,?,?,?,?)');
                $ins->execute([$id, 'entrega', $s_logradouro, $s_numero, $s_complemento, $s_bairro, $s_cidade, $s_estado, $s_cep]);
                $addrId = (int)$pdo->lastInsertId();
                $geo = $this->geocodeAddress(['logradouro'=>$s_logradouro,'numero'=>$s_numero,'bairro'=>$s_bairro,'cidade'=>$s_cidade,'estado'=>$s_estado,'cep'=>$s_cep]);
                if ($geo) { $pdo->prepare('UPDATE client_addresses SET latitude = ?, longitude = ? WHERE id = ?')->execute([$geo['lat'], $geo['lon'], $addrId]); }
            }
        }