#!/usr/bin/env python3
"""
Sistema de Remoção de Marca d'Água — Olímpia House
Versão 3.1 — Suporte a múltiplos templates de logo

Estratégias de detecção (em ordem de prioridade):
  1. TEMPLATE MATCHING: você fornece UMA OU MAIS imagens de logo
     (cada foto é testada contra todos os templates; o melhor match vence)
  2. DETECÇÃO POR COR: encontra automaticamente pela co-ocorrência
     de cores específicas (azul-escuro + vermelho do logo)

Estratégias de inpainting:
  1. LAMA AI (--ai): qualidade fotorrealística, requer instalação
  2. OPENCV ITERATIVO (padrão): qualidade boa, sempre funciona
"""

import cv2
import numpy as np
import argparse
import sys
from pathlib import Path


# ============================================================================
# DETECÇÃO DA MARCA D'ÁGUA
# ============================================================================

def detectar_por_template(imagem, template_path, threshold=0.7):
    """
    Detecta o logo usando template matching — preciso quando você tem
    o logo como imagem separada. Funciona com escalas variáveis.
    
    Retorna (x, y, largura, altura, score) ou None.
    """
    template = cv2.imread(str(template_path))
    if template is None:
        return None
    
    img_gray = cv2.cvtColor(imagem, cv2.COLOR_BGR2GRAY)
    tpl_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
    
    h, w = imagem.shape[:2]
    th, tw = tpl_gray.shape
    
    melhor_match = None
    melhor_score = 0
    melhor_escala = 1.0
    
    # Testar múltiplas escalas (logo pode ter tamanhos diferentes)
    for escala in np.linspace(0.3, 1.5, 25):
        nova_w = int(tw * escala)
        nova_h = int(th * escala)
        if nova_w > w or nova_h > h or nova_w < 30 or nova_h < 30:
            continue
        
        tpl_redim = cv2.resize(tpl_gray, (nova_w, nova_h))
        resultado = cv2.matchTemplate(img_gray, tpl_redim, cv2.TM_CCOEFF_NORMED)
        _, max_val, _, max_loc = cv2.minMaxLoc(resultado)
        
        if max_val > melhor_score:
            melhor_score = max_val
            melhor_match = max_loc
            melhor_escala = escala
    
    if melhor_score < threshold:
        return None
    
    x, y = melhor_match
    largura = int(tw * melhor_escala)
    altura = int(th * melhor_escala)
    return (x, y, largura, altura, melhor_score)


def detectar_por_multiplos_templates(imagem, template_paths, threshold=0.7):
    """
    Testa vários templates contra a mesma imagem e retorna o melhor match.
    
    Útil quando você tem versões diferentes do logo (logo antigo, logo
    novo, variações de cor, etc.) e quer limpar todos em uma única passada
    sem precisar separar as fotos por tipo de logo.
    
    Args:
        imagem: imagem em formato BGR (OpenCV)
        template_paths: lista de caminhos para imagens de logo
        threshold: score mínimo (0.0 a 1.0) para considerar match válido
    
    Returns:
        (x, y, largura, altura, score, nome_template) ou None se nada bateu
    """
    melhor_bbox = None
    melhor_score = 0
    melhor_nome = None
    
    for tpl_path in template_paths:
        tpl_path = Path(tpl_path)
        if not tpl_path.exists():
            continue
        
        # Reaproveita a função de template único
        # (usa threshold baixo aqui pra não filtrar nada — filtragem é
        # feita no final, comparando entre todos os candidatos)
        resultado = detectar_por_template(imagem, tpl_path, threshold=0.0)
        if resultado is None:
            continue
        
        score = resultado[4]
        if score > melhor_score:
            melhor_score = score
            melhor_bbox = resultado
            melhor_nome = tpl_path.name
    
    if melhor_bbox is None or melhor_score < threshold:
        return None
    
    x, y, cw, ch, score = melhor_bbox
    return (x, y, cw, ch, score, melhor_nome)


def detectar_por_cor_logo_olimpia(imagem, debug=False):
    """
    Detecção específica para o logo da Olímpia House.
    Procura região onde co-ocorrem azul-escuro (#1a2942 aprox) e vermelho.
    """
    h, w = imagem.shape[:2]
    hsv = cv2.cvtColor(imagem, cv2.COLOR_BGR2HSV)
    
    # Azul escuro do logo
    mask_azul = cv2.inRange(hsv,
                            np.array([100, 100, 30]),
                            np.array([130, 255, 120]))
    
    # Vermelho do logo (duas faixas porque vermelho wrappa o hue)
    mask_vermelho = (cv2.inRange(hsv, np.array([0, 100, 100]), np.array([10, 255, 255]))
                    | cv2.inRange(hsv, np.array([170, 100, 100]), np.array([180, 255, 255])))
    
    # A chave: dilatar ambas e achar onde se sobrepõem
    # Logo tem AS DUAS cores próximas; objetos casuais geralmente têm só uma
    kernel = np.ones((30, 30), np.uint8)
    azul_d = cv2.dilate(mask_azul, kernel)
    verm_d = cv2.dilate(mask_vermelho, kernel)
    overlap = cv2.bitwise_and(azul_d, verm_d)
    
    contornos, _ = cv2.findContours(overlap, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not contornos:
        return None
    
    # Filtrar por área e proporção (logo é aproximadamente quadrado)
    candidatos = []
    for c in contornos:
        x, y, cw, ch = cv2.boundingRect(c)
        area = cw * ch
        # Logo geralmente é 5%-25% da imagem e quase quadrado
        if area < (w * h * 0.005) or area > (w * h * 0.35):
            continue
        aspect = cw / ch if ch > 0 else 0
        if not (0.6 <= aspect <= 1.6):
            continue
        # Score: combina tamanho razoável e quadratura
        score = area * (1 - abs(1 - aspect))
        candidatos.append((score, x, y, cw, ch))
    
    if not candidatos:
        return None
    
    candidatos.sort(reverse=True)
    _, x, y, cw, ch = candidatos[0]
    return (x, y, cw, ch, 1.0)


def detectar_por_area_central(imagem, percentual=0.20):
    """
    Não detecta nada — apenas retorna a área central da imagem como bbox.
    
    Útil quando você sabe que o logo está sempre no centro de todas as fotos
    (pré-processamento padronizado da imobiliária). É a abordagem mais rápida
    e a mais confiável quando essa premissa é verdadeira: zero falsos negativos,
    zero falsos positivos.
    
    Args:
        imagem: imagem em formato BGR
        percentual: tamanho do lado do quadrado central em fração da LARGURA
                    da imagem (0.20 = 20%). Pra uma foto 1200x800, gera um
                    quadrado de 240x240 (20% de 1200) centralizado.
    
    Returns:
        (x, y, largura, altura, score=1.0) — score fixo em 1.0 porque
        não é uma detecção, é uma posição conhecida.
    """
    h, w = imagem.shape[:2]
    
    # Lado do quadrado: % da LARGURA da imagem.
    # (Usamos a largura porque logos costumam ser dimensionados em proporção
    #  horizontal e isso casa com o que o usuário percebe como "X% da imagem".)
    lado = int(w * percentual)
    
    # Garantir que cabe na altura também
    lado = min(lado, h)
    
    # Centralizar
    x = (w - lado) // 2
    y = (h - lado) // 2
    
    return (x, y, lado, lado, 1.0)


def construir_mascara(imagem_shape, bbox, expansao_px=18):
    """Cria máscara binária expandida para cobrir borda da marca."""
    h, w = imagem_shape[:2]
    x, y, cw, ch = bbox[:4]
    x1 = max(0, x - expansao_px)
    y1 = max(0, y - expansao_px)
    x2 = min(w, x + cw + expansao_px)
    y2 = min(h, y + ch + expansao_px)
    
    mask = np.zeros((h, w), dtype=np.uint8)
    mask[y1:y2, x1:x2] = 255
    return mask, (x1, y1, x2, y2)


# ============================================================================
# INPAINTING — RECONSTRUÇÃO DA ÁREA
# ============================================================================

def inpaint_opencv_otimizado(imagem, mask):
    """
    Borrão radial ISOTRÓPICO (sem direção preferencial) com gradiente
    gaussiano — sem forma quadrada nem padrão de X.
    
    Estratégia:
      1. Gera várias versões da região com BLUR puro (Gaussiano), com
         diferentes intensidades.
      2. Mistura essas versões usando peso radial: centro pega o blur
         mais forte, bordas pegam o blur mais fraco, e ao final tudo
         dissolve no original.
    
    Por que isso elimina o X do spin blur: blur Gaussiano é isotrópico
    (mesmo em todas as direções), então não há cantos ou ângulos que
    se acumulem como acontecia na rotação.
    """
    h, w = imagem.shape[:2]
    
    # Achar centro e raio efetivo da área
    ys, xs = np.where(mask > 0)
    if len(xs) == 0:
        return imagem
    
    cx = (xs.min() + xs.max()) // 2
    cy = (ys.min() + ys.max()) // 2
    raio_logo = max(xs.max() - xs.min(), ys.max() - ys.min()) // 2
    
    # ETAPA 1: Recortar área generosa pra processar
    margem = int(raio_logo * 1.0)
    x1 = max(0, cx - raio_logo - margem)
    y1 = max(0, cy - raio_logo - margem)
    x2 = min(w, cx + raio_logo + margem)
    y2 = min(h, cy + raio_logo + margem)
    
    regiao = imagem[y1:y2, x1:x2].copy()
    rh, rw = regiao.shape[:2]
    centro_local = (rw // 2, rh // 2)
    
    # ETAPA 2: Criar TRÊS níveis de blur Gaussiano da região
    # (do mais sutil ao mais forte). Usaremos cada um em uma "camada"
    # radial diferente.
    
    sigma_leve = max(5, int(raio_logo * 0.10))
    sigma_medio = max(10, int(raio_logo * 0.25))
    sigma_forte = max(20, int(raio_logo * 0.50))
    
    def gauss(img, sigma):
        k = sigma * 4 + 1
        if k % 2 == 0:
            k += 1
        return cv2.GaussianBlur(img, (k, k), sigma)
    
    blur_leve = gauss(regiao, sigma_leve)
    blur_medio = gauss(regiao, sigma_medio)
    blur_forte = gauss(regiao, sigma_forte)
    
    # ETAPA 3: Mapa de distância radial (do centro do logo)
    yy, xx = np.meshgrid(np.arange(rh), np.arange(rw), indexing='ij')
    distancia = np.sqrt((xx - centro_local[0]) ** 2 +
                        (yy - centro_local[1]) ** 2).astype(np.float32)
    
    # Distância normalizada (0 = centro, 1 = raio do logo)
    d_norm = distancia / max(raio_logo, 1)
    
    # ETAPA 4: Pesos para cada camada de blur
    # - Centro do logo (d=0):       peso forte = 1.0, médio = 0,    leve = 0
    # - Borda do logo (d=1):        peso forte = 0.3, médio = 0.5,  leve = 0.2
    # - Halo externo (d=1.5):       peso forte = 0,   médio = 0.2,  leve = 0.4
    # - Longe (d>2.5):              tudo zero (volta original)
    
    # Curvas gaussianas centradas em diferentes raios:
    peso_forte = np.exp(-(d_norm ** 2) / (2 * 0.50 ** 2))   # pico no centro
    peso_medio = np.exp(-((d_norm - 0.5) ** 2) / (2 * 0.40 ** 2))  # pico em meio raio
    peso_leve  = np.exp(-((d_norm - 1.0) ** 2) / (2 * 0.50 ** 2))  # pico na borda do logo
    
    # Ruído de baixa frequência para tornar o "blob" não-circular
    np.random.seed(7)
    ruido = np.random.randn(rh, rw).astype(np.float32)
    ruido = cv2.GaussianBlur(ruido, (61, 61), 30)
    if ruido.max() > ruido.min():
        ruido = (ruido - ruido.min()) / (ruido.max() - ruido.min()) - 0.5
        ruido *= 0.20
    
    peso_forte = np.clip(peso_forte + ruido * peso_forte, 0, 1)
    peso_medio = np.clip(peso_medio + ruido * peso_medio, 0, 1)
    peso_leve  = np.clip(peso_leve  + ruido * peso_leve,  0, 1)
    
    # Suavizar mapas (continuidade absoluta entre camadas)
    peso_forte = cv2.GaussianBlur(peso_forte, (31, 31), 10)
    peso_medio = cv2.GaussianBlur(peso_medio, (31, 31), 10)
    peso_leve  = cv2.GaussianBlur(peso_leve,  (31, 31), 10)
    
    # Normalizar: soma dos pesos define o quanto "blur" entra,
    # e o que sobrar é a imagem original
    soma_pesos = peso_forte + peso_medio + peso_leve
    soma_pesos = np.clip(soma_pesos, 0, 1)  # nunca passa de 1.0
    
    # Renormalizar entre as camadas para distribuição relativa
    soma_relativa = np.maximum(peso_forte + peso_medio + peso_leve, 1e-6)
    w_forte = peso_forte / soma_relativa
    w_medio = peso_medio / soma_relativa
    w_leve  = peso_leve  / soma_relativa
    
    # Combinar as 3 camadas blurradas
    blur_combinado = (w_forte[:, :, np.newaxis] * blur_forte.astype(np.float32) +
                      w_medio[:, :, np.newaxis] * blur_medio.astype(np.float32) +
                      w_leve[:, :, np.newaxis]  * blur_leve.astype(np.float32))
    
    # ETAPA 5: Blend final entre blur combinado e original
    # soma_pesos define quanto do blur entra na composição
    peso_blur = soma_pesos[:, :, np.newaxis]
    
    regiao_blendada = (regiao.astype(np.float32) * (1 - peso_blur) +
                       blur_combinado * peso_blur)
    
    # Reinserir na imagem original
    resultado = imagem.copy()
    resultado[y1:y2, x1:x2] = np.clip(regiao_blendada, 0, 255).astype(np.uint8)
    
    return resultado


def inpaint_com_ia_lama(imagem, mask):
    """
    Inpainting com modelo IA LaMa (Resolution-robust Large Mask Inpainting).
    Qualidade fotorrealística. Requer: pip install iopaint
    
    Na primeira execução faz download do modelo (~200 MB).
    """
    try:
        from iopaint.model_manager import ModelManager
        from iopaint.schema import InpaintRequest, HDStrategy
    except ImportError:
        print("ERRO: biblioteca 'iopaint' não instalada.")
        print("Para usar IA, instale com: pip install iopaint")
        print("Por ora usando OpenCV otimizado como fallback.\n")
        return inpaint_opencv_otimizado(imagem, mask)
    
    try:
        model = ModelManager(name="lama", device="cpu")
        config = InpaintRequest(hd_strategy=HDStrategy.ORIGINAL)
        # iopaint espera RGB
        img_rgb = cv2.cvtColor(imagem, cv2.COLOR_BGR2RGB)
        resultado_rgb = model(img_rgb, mask, config)
        return cv2.cvtColor(resultado_rgb, cv2.COLOR_RGB2BGR)
    except Exception as e:
        print(f"AVISO: IA falhou ({e}), usando OpenCV otimizado como fallback.\n")
        return inpaint_opencv_otimizado(imagem, mask)


# ============================================================================
# PIPELINE PRINCIPAL
# ============================================================================

def processar_imagem(caminho_entrada, caminho_saida, template_paths=None,
                     usar_ia=False, area_central=None, verbose=True):
    """Pipeline completo: detectar + remover + salvar.
    
    Args:
        template_paths: lista de caminhos de templates (ou None)
        area_central: se fornecido (ex: 0.20), pula detecção e usa área
                      central com esse percentual. Modo recomendado quando
                      o logo está sempre centralizado em todas as fotos.
    """
    
    imagem = cv2.imread(str(caminho_entrada))
    if imagem is None:
        print(f"ERRO: não consegui ler {caminho_entrada}")
        return False
    
    h, w = imagem.shape[:2]
    if verbose:
        print(f"  Processando: {Path(caminho_entrada).name} ({w}x{h}px)")
    
    # Estratégia de localização da marca
    bbox = None
    metodo = None
    
    # PRIORIDADE 1: área central fixa (se solicitado explicitamente)
    # — não tenta detectar nada, simplesmente usa o centro
    if area_central is not None:
        bbox = detectar_por_area_central(imagem, percentual=area_central)
        metodo = f"área central ({area_central:.0%} da largura)"
    
    # PRIORIDADE 2: template matching
    elif template_paths:
        templates_validos = [p for p in template_paths if Path(p).exists()]
        if templates_validos:
            resultado = detectar_por_multiplos_templates(imagem, templates_validos)
            if resultado:
                x, y, cw, ch, score, nome = resultado
                bbox = (x, y, cw, ch, score)
                metodo = f"template '{nome}' (confiança {score:.0%})"
    
    # PRIORIDADE 3: fallback por cor (só se modo central NÃO foi usado)
    if bbox is None and area_central is None:
        bbox = detectar_por_cor_logo_olimpia(imagem)
        if bbox:
            metodo = "cor (azul+vermelho)"
    
    if bbox is None:
        if verbose:
            print(f"    ⚠ Marca d'água não detectada — pulando arquivo")
        return False
    
    if verbose:
        x, y, cw, ch = bbox[:4]
        print(f"    ✓ Marca detectada em ({x},{y}) tamanho {cw}x{ch} via {metodo}")
    
    # Construir máscara
    mask, (x1, y1, x2, y2) = construir_mascara(imagem.shape, bbox, expansao_px=18)
    
    # Aplicar inpainting
    if usar_ia:
        if verbose:
            print(f"    → Reconstruindo com IA (LaMa)...")
        resultado = inpaint_com_ia_lama(imagem, mask)
    else:
        if verbose:
            print(f"    → Reconstruindo com OpenCV otimizado...")
        resultado = inpaint_opencv_otimizado(imagem, mask)
    
    # Salvar
    Path(caminho_saida).parent.mkdir(parents=True, exist_ok=True)
    sucesso = cv2.imwrite(str(caminho_saida), resultado,
                          [cv2.IMWRITE_JPEG_QUALITY, 95])
    
    if verbose:
        if sucesso:
            print(f"    ✓ Salvo em: {caminho_saida}")
        else:
            print(f"    ✗ Erro ao salvar")
    
    return sucesso


def processar_lote(pasta_entrada, pasta_saida, template_paths=None,
                   usar_ia=False, area_central=None):
    """Processa todas as imagens de uma pasta."""
    
    entrada = Path(pasta_entrada)
    saida = Path(pasta_saida)
    saida.mkdir(parents=True, exist_ok=True)
    
    extensoes = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'}
    arquivos = [f for f in entrada.iterdir()
                if f.is_file() and f.suffix.lower() in extensoes]
    
    if not arquivos:
        print(f"Nenhuma imagem encontrada em {entrada}")
        return
    
    print(f"\nEncontradas {len(arquivos)} imagens. Iniciando processamento...")
    if area_central is not None:
        print(f"Modo: ÁREA CENTRAL FIXA ({area_central:.0%} da largura)")
    elif template_paths:
        templates_validos = [p for p in template_paths if Path(p).exists()]
        if templates_validos:
            print(f"Templates carregados ({len(templates_validos)}):")
            for tp in templates_validos:
                print(f"  • {Path(tp).name}")
        templates_invalidos = [p for p in template_paths if not Path(p).exists()]
        if templates_invalidos:
            print(f"AVISO: templates não encontrados (ignorados):")
            for tp in templates_invalidos:
                print(f"  • {tp}")
    print()
    
    sucessos = 0
    falhas = 0
    for i, arq in enumerate(arquivos, 1):
        print(f"[{i}/{len(arquivos)}]", end=" ")
        saida_arq = saida / arq.name
        if processar_imagem(arq, saida_arq, template_paths, usar_ia, area_central):
            sucessos += 1
        else:
            falhas += 1
        print()
    
    print("=" * 60)
    print(f"  Total:   {len(arquivos)}")
    print(f"  Sucesso: {sucessos}")
    print(f"  Falhas:  {falhas}")
    print("=" * 60)


# ============================================================================
# CLI
# ============================================================================

def processar_recursivo(pasta_raiz, template_paths=None, area_central=None,
                        usar_ia=False, log_path=None, dry_run=False):
    """
    Varre recursivamente uma pasta processando TODAS as imagens encontradas
    em todos os níveis de subpastas, substituindo cada arquivo no lugar
    de forma segura.

    Segurança:
      - Processa pra arquivo temporário .{nome}.tmp na mesma pasta
      - Só substitui o original se a saída foi gerada com sucesso e tem
        tamanho razoável (>1KB)
      - Em caso de erro durante o salvamento, remove o .tmp e mantém
        o original intacto
      - Mantém log de imagens já processadas pra permitir retomar de onde
        parou (útil em VPS — se SSH cair, é só rodar de novo)

    Args:
        pasta_raiz: pasta inicial (ex: /home/server/xml/imagens)
        template_paths: lista de templates ou None
        area_central: % para modo --center, ou None
        usar_ia: usa LaMa (--ai)
        log_path: caminho do arquivo de log de processadas
        dry_run: só lista o que seria feito, sem modificar nada
    """
    import time

    raiz = Path(pasta_raiz)
    if not raiz.is_dir():
        print(f"ERRO: '{raiz}' não é uma pasta válida")
        return

    if log_path is None:
        log_path = raiz / ".processadas.log"
    log_path = Path(log_path)

    # Carregar log de imagens já processadas (pra retomar de onde parou)
    ja_processadas = set()
    if log_path.exists():
        with open(log_path, 'r', encoding='utf-8') as f:
            ja_processadas = {linha.strip() for linha in f if linha.strip()}
        print(f"Log existente encontrado: {len(ja_processadas)} arquivos já processados anteriormente")

    extensoes = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'}

    # Listar todas as imagens recursivamente
    print(f"Varrendo {raiz} recursivamente...")
    todas = []
    for arq in raiz.rglob('*'):
        if arq.is_file() and arq.suffix.lower() in extensoes and not arq.name.startswith('.'):
            todas.append(arq)

    if not todas:
        print(f"Nenhuma imagem encontrada em {raiz}")
        return

    pendentes = [a for a in todas if str(a) not in ja_processadas]

    print(f"Total de imagens:       {len(todas)}")
    print(f"Já processadas:         {len(todas) - len(pendentes)}")
    print(f"Pendentes:              {len(pendentes)}")
    print()

    if dry_run:
        print("[DRY-RUN] Nenhum arquivo será modificado. Listagem das pendentes:")
        for p in pendentes[:20]:
            print(f"  • {p}")
        if len(pendentes) > 20:
            print(f"  ... e mais {len(pendentes) - 20} arquivos")
        return

    if not pendentes:
        print("Nada a processar — tudo já foi feito.")
        return

    sucessos = 0
    falhas = 0
    nao_detectadas = 0
    inicio = time.time()

    # Abrir log em modo append (escreve o nome assim que termina cada arquivo)
    with open(log_path, 'a', encoding='utf-8') as log_f:
        for i, arq in enumerate(pendentes, 1):
            # Caminho temporário na mesma pasta do original.
            # IMPORTANTE: o cv2.imwrite usa a extensão do arquivo para
            # decidir o codec, então o .tmp tem que vir ANTES da extensão.
            # Resultado: ".OH10053_001.tmp.jpg" (oculto + tmp + extensão)
            tmp_path = arq.parent / f".{arq.stem}.tmp{arq.suffix}"

            # Estimar tempo restante
            if i > 1:
                elapsed = time.time() - inicio
                avg = elapsed / (i - 1)
                restante = avg * (len(pendentes) - i + 1)
                eta = f" — ETA {int(restante//60)}m{int(restante%60)}s"
            else:
                eta = ""

            print(f"[{i}/{len(pendentes)}]{eta} {arq.relative_to(raiz)}")

            try:
                # Processar para arquivo temporário
                ok = processar_imagem(
                    str(arq), str(tmp_path),
                    template_paths=template_paths,
                    area_central=area_central,
                    usar_ia=usar_ia,
                    verbose=False  # menos verboso em modo lote
                )

                if not ok:
                    # Marca não detectada — apenas registra e segue
                    nao_detectadas += 1
                    print(f"    ⚠ Marca não detectada, arquivo mantido como está")
                    if tmp_path.exists():
                        tmp_path.unlink()
                    log_f.write(f"{arq}\n")
                    log_f.flush()
                    continue

                # Validar saída antes de substituir
                if not tmp_path.exists() or tmp_path.stat().st_size < 1024:
                    print(f"    ✗ Saída inválida ou muito pequena, descartando")
                    if tmp_path.exists():
                        tmp_path.unlink()
                    falhas += 1
                    continue

                # Substituir o original pelo processado (atomicamente)
                # os.replace funciona inclusive entre filesystems no Linux
                import os as _os
                _os.replace(str(tmp_path), str(arq))

                sucessos += 1
                print(f"    ✓ Substituído")

                # Registrar no log imediatamente (permite retomar)
                log_f.write(f"{arq}\n")
                log_f.flush()

            except KeyboardInterrupt:
                # CTRL+C — limpa o temp se existir e sai limpo
                print("\n\nInterrompido pelo usuário. Limpando arquivo temporário...")
                if tmp_path.exists():
                    tmp_path.unlink()
                print(f"Para retomar de onde parou, rode o mesmo comando novamente.")
                print(f"Log salvo em: {log_path}")
                return

            except Exception as e:
                print(f"    ✗ Erro: {e}")
                if tmp_path.exists():
                    try:
                        tmp_path.unlink()
                    except Exception:
                        pass
                falhas += 1

    elapsed = time.time() - inicio
    print()
    print("=" * 60)
    print(f"  Pendentes processadas:  {len(pendentes)}")
    print(f"  Sucesso:                {sucessos}")
    print(f"  Marca não detectada:    {nao_detectadas}")
    print(f"  Falhas:                 {falhas}")
    print(f"  Tempo total:            {int(elapsed//60)}m{int(elapsed%60)}s")
    if sucessos + nao_detectadas > 0:
        print(f"  Média por imagem:       {elapsed/(sucessos+nao_detectadas):.2f}s")
    print(f"  Log salvo em:           {log_path}")
    print("=" * 60)


def coletar_templates(args_template, args_templates_dir):
    """
    Reúne todos os templates fornecidos pelo usuário em uma única lista.
    
    Aceita três formas (podem ser combinadas):
      --template logo1.png --template logo2.png   (repetir flag)
      --template logo1.png,logo2.png              (separados por vírgula)
      --templates-dir ./logos                     (todos os arquivos da pasta)
    """
    paths = []
    
    # Flag --template (pode ser repetida, e cada uma pode ter vírgulas)
    if args_template:
        for entry in args_template:
            for sub in entry.split(','):
                sub = sub.strip()
                if sub:
                    paths.append(sub)
    
    # Flag --templates-dir (pasta com logos)
    if args_templates_dir:
        pasta = Path(args_templates_dir)
        if not pasta.is_dir():
            print(f"AVISO: --templates-dir '{pasta}' não é uma pasta válida")
        else:
            extensoes = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
            for arq in sorted(pasta.iterdir()):
                if arq.is_file() and arq.suffix.lower() in extensoes:
                    paths.append(str(arq))
    
    # Remover duplicatas preservando ordem
    vistos = set()
    unicos = []
    for p in paths:
        if p not in vistos:
            vistos.add(p)
            unicos.append(p)
    return unicos


def main():
    parser = argparse.ArgumentParser(
        description="Remove marca d'água Olímpia House de imagens de imóveis",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Exemplos:

  # Processar uma imagem (detecção automática por cor)
  python remove_marca.py -i casa.jpg -o casa_limpa.jpg

  # Processar pasta inteira
  python remove_marca.py -d ./fotos -o ./fotos_limpas

  # ÁREA CENTRAL FIXA: quando o logo sempre está no centro
  # (mais rápido e confiável; sem detecção, sem falsos positivos)
  python remove_marca.py -d ./fotos -o ./limpas --center

  # Ajustar tamanho da área central (padrão é 20%)
  python remove_marca.py -d ./fotos -o ./limpas --center 0.25

  # Usar UM template
  python remove_marca.py -d ./fotos -o ./limpas --template logo.png

  # Usar VÁRIOS templates (repetindo a flag)
  python remove_marca.py -d ./fotos -o ./limpas --template logo_antigo.png --template logo_novo.png

  # Usar VÁRIOS templates (separados por vírgula)
  python remove_marca.py -d ./fotos -o ./limpas --template logo_antigo.png,logo_novo.png

  # Usar VÁRIOS templates (pasta inteira)
  python remove_marca.py -d ./fotos -o ./limpas --templates-dir ./meus_logos

  # Combinar com IA LaMa para qualidade fotorrealística
  python remove_marca.py -d ./fotos -o ./limpas --center --ai

  # RECURSIVO (varre todas as subpastas, substitui originais no lugar)
  # — Ideal para processar /home/server/xml/imagens/ inteiro no VPS.
  # — Pode interromper com CTRL+C e retomar de onde parou.
  python remove_marca.py -r /home/server/xml/imagens --center

  # Recursivo com IA
  python remove_marca.py -r /home/server/xml/imagens --center --ai

  # Dry-run: lista o que SERIA feito sem modificar nada (recomendado
  # antes da primeira execução para conferir a contagem de arquivos)
  python remove_marca.py -r /home/server/xml/imagens --center --dry-run
""")
    parser.add_argument('-i', '--input', help='Arquivo de entrada (imagem única)')
    parser.add_argument('-o', '--output', help='Arquivo ou pasta de saída')
    parser.add_argument('-d', '--directory', help='Pasta com várias imagens')
    parser.add_argument('-r', '--recursive', metavar='PASTA',
                       help='Processa recursivamente todas as imagens de PASTA '
                            'e subpastas, SUBSTITUINDO os arquivos originais no '
                            'lugar. Mantém log para permitir retomada após '
                            'interrupção. Recomendado: combinar com --center.')
    parser.add_argument('--dry-run', action='store_true',
                       help='Apenas lista os arquivos que SERIAM processados, '
                            'sem modificar nada. Use junto com -r para conferir '
                            'antes de rodar.')
    parser.add_argument('--template', action='append',
                       help='Imagem do logo para template matching. '
                            'Pode ser usado múltiplas vezes ou ter valores '
                            'separados por vírgula.')
    parser.add_argument('--templates-dir',
                       help='Pasta contendo várias imagens de logo — '
                            'todos os arquivos de imagem da pasta são usados.')
    parser.add_argument('--center', nargs='?', const=0.20, type=float,
                       default=None, metavar='PERCENTUAL',
                       help='Remove a área central quadrada (sem detectar). '
                            'PERCENTUAL é uma fração da largura da imagem. '
                            'Padrão: 0.20 (20%%). Exemplo: --center 0.25')
    parser.add_argument('--ai', action='store_true',
                       help='Usar IA LaMa (requer: pip install iopaint)')
    
    args = parser.parse_args()
    
    # Validar --center
    if args.center is not None:
        if not (0.05 <= args.center <= 0.60):
            print(f"ERRO: --center precisa estar entre 0.05 e 0.60 (você passou {args.center})")
            sys.exit(1)
    
    # Coletar todos os templates fornecidos
    templates = coletar_templates(args.template, args.templates_dir)
    templates = templates if templates else None
    
    # Avisar se conflitarem (center tem prioridade, mas avisa o usuário)
    if args.center is not None and templates:
        print("AVISO: --center foi usado, ignorando os templates fornecidos.\n")
        templates = None
    
    if args.input:
        saida = args.output or args.input.replace('.', '_limpa.')
        if args.center is not None:
            print(f"Modo: ÁREA CENTRAL FIXA ({args.center:.0%} da largura)\n")
        elif templates:
            templates_validos = [p for p in templates if Path(p).exists()]
            if templates_validos:
                print(f"Usando {len(templates_validos)} template(s):")
                for tp in templates_validos:
                    print(f"  • {Path(tp).name}")
                print()
        ok = processar_imagem(args.input, saida, templates, args.ai, args.center)
        sys.exit(0 if ok else 1)
    
    elif args.recursive:
        # Modo recursivo: varre subpastas e substitui originais
        if args.center is not None:
            print(f"Modo: RECURSIVO com ÁREA CENTRAL FIXA ({args.center:.0%} da largura)")
        elif templates:
            print(f"Modo: RECURSIVO com {len(templates)} template(s)")
        else:
            print("Modo: RECURSIVO com detecção automática por cor")
        if args.ai:
            print("Inpainting: IA LaMa")
        else:
            print("Inpainting: OpenCV (blur radial isotrópico)")
        print(f"Pasta raiz: {args.recursive}")
        print(f"ATENÇÃO: este modo SUBSTITUI os arquivos originais no lugar.")
        if args.dry_run:
            print("(dry-run ativado — nada será modificado)")
        print()
        
        processar_recursivo(
            pasta_raiz=args.recursive,
            template_paths=templates,
            area_central=args.center,
            usar_ia=args.ai,
            dry_run=args.dry_run
        )
    
    elif args.directory:
        if not args.output:
            print("ERRO: ao usar -d, é obrigatório também passar -o (pasta de saída)")
            sys.exit(1)
        processar_lote(args.directory, args.output, templates, args.ai, args.center)
    
    else:
        parser.print_help()


if __name__ == '__main__':
    main()
