#!/usr/bin/env python3
"""
Revenda+ Powered by Urafiki
cron/processar_identificacao.py

Cron de validação automática de identidade.
Disparado imediatamente após upload das fotos (chamado pelo PHP).
Também pode correr como cron a cada 5 minutos para processar pendentes.

Uso:
  python3 processar_identificacao.py --msisdn 258873826690
  python3 processar_identificacao.py --pendentes

Lógica:
  1. Ler caminhos das fotos em common/identificacao/{msisdn}/
  2. OCR do BI (frente + verso) via Tesseract
  3. Face match (selfie vs foto extraída do BI) via OpenCV
  4. Se OCR OK + match >= threshold → estadoId=5 + notifica
  5. Se falha → fica em estadoId=4 para revisão humana
"""

import sys
import os
import re
import cv2
import subprocess
import json
import argparse
import pymysql
import pymysql.cursors
from datetime import datetime
from pathlib import Path

# =============================================================================
# CONFIGURAÇÃO
# =============================================================================

BASE_DIR       = Path(__file__).parent.parent
ID_DIR         = BASE_DIR / 'common' / 'identificacao'
LOG_FILE       = BASE_DIR / 'runtime' / 'logs' / 'identificacao.log'

# Thresholds de face match
THRESHOLD_AUTO    = 65.0   # >= auto-aprova → estadoId=5
THRESHOLD_INCERTO = 40.0   # >= revisão humana → estadoId=4
# < THRESHOLD_INCERTO → alerta mismatch → estadoId=4 com flag

# Estados
ESTADO_PENDENTE    = 2
ESTADO_VALIDACAO   = 4
ESTADO_VALIDADO    = 5

# DB — ler do ficheiro de conexão PHP (parse simples)
def ler_credenciais_db():
    conn_file = BASE_DIR / 'common' / 'connections' / 'revenda.urafiki.co.mz.php'
    content = conn_file.read_text()
    host = re.search(r"mysqli_connect\(['\"]([^'\"]+)['\"]", content)
    user = re.search(r"mysqli_connect\([^,]+,\s*['\"]([^'\"]+)['\"]", content)
    pwd  = re.search(r"mysqli_connect\([^,]+,[^,]+,\s*['\"]([^'\"]*)['\"]", content)
    db   = re.search(r"mysqli_connect\([^,]+,[^,]+,[^,]+,\s*['\"]([^'\"]+)['\"]", content)
    return {
        'host': host.group(1) if host else '196.3.101.58',
        'user': user.group(1) if user else 'ura',
        'passwd': pwd.group(1) if pwd else '#Ur@f1ki2022#',
        'db': db.group(1) if db else 'revenda.urafiki.co.mz',
    }

def conectar_db():
    creds = ler_credenciais_db()
    return pymysql.connect(
        host=creds['host'],
        user=creds['user'],
        password=creds['passwd'],
        database=creds['db'],
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor
    )

def log(msg):
    ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    linha = f"[{ts}] {msg}"
    print(linha)
    try:
        LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, 'a') as f:
            f.write(linha + '\n')
    except:
        pass

# =============================================================================
# OCR
# =============================================================================

def ocr_imagem(caminho: str) -> str:
    """Corre Tesseract com múltiplos PSMs e retorna o melhor output."""
    melhor = ''
    melhor_hits = 0
    keywords = ['MOCAMBIQUE','IDENTIDADE','NOME','NUIT','NASCIMENTO','MAPUTO',
                'VALIDADE','EMISSAO','IDMOZ','CASADO','SOLTEIRO']

    for psm in [3, 1, 6]:
        resultado = subprocess.run(
            ['tesseract', caminho, 'stdout', '-l', 'eng', f'--psm', str(psm), '--oem', '3'],
            capture_output=True, text=True, timeout=30
        )
        texto = resultado.stdout
        hits = sum(1 for k in keywords if k in texto.upper())
        if hits > melhor_hits:
            melhor_hits = hits
            melhor = texto

    return melhor

def parsear_frente(texto: str) -> dict:
    """Extrai campos da frente do BI moçambicano."""
    dados = {}

    # Nome — linha em maiúsculas após "Nome / Name:"
    m = re.search(r'Nome\s*/\s*Name:\s*\n+([A-Z][A-Z\s]+?)\n', texto)
    if m: dados['nomeCompleto'] = m.group(1).strip()

    # Nº BI — formato moçambicano: 12 dígitos + 1 letra MAIÚSCULA
    # Obrigatoriamente precedido de N° / Nº / N2
    # Rejeita campos sem label como AA9401239
    m = re.search(r'N[°oº²]\s*:?\s*(\d{10,13}[A-Z])', texto, re.IGNORECASE)
    if m:
        dados['numeroBi'] = m.group(1).strip()
    else:
        # Fallback — sequência isolada de 12 dígitos + 1 letra
        m = re.search(r'(?<!\w)(\d{12}[A-Z])(?!\w)', texto)
        if m: dados['numeroBi'] = m.group(1).strip()

    # Data nascimento — primeiro DD/MM/YYYY encontrado
    m = re.search(r'(\d{2}/\d{2}/\d{4})', texto)
    if m:
        d = m.group(1)
        dados['dataNascimento'] = f"{d[6:]}-{d[3:5]}-{d[0:2]}"

    # Altura
    m = re.search(r'Altura\s*/\s*Height:\s*([\d,\.]+\s*m)', texto)
    if m: dados['altura'] = m.group(1).strip()

    # Sexo
    m = re.search(r'Sexo\s*/\s*Sex:\s*([MF])\b', texto)
    if m: dados['sexo'] = m.group(1).strip()

    # Naturalidade
    m = re.search(r'Place of Birth:\s*\n([A-Z][A-Z\s]+?)\n', texto)
    if m: dados['naturalidade'] = m.group(1).strip()

    # Morada
    m = re.search(r'Address:\s*\n+(.+?)\n+(.+?)\n+(.+?)\s*Assinatura', texto, re.DOTALL)
    if m:
        morada = f"{m.group(1).strip()}, {m.group(2).strip()}, {m.group(3).strip()}"
        morada = re.sub(r'KAMPFU\s+MO', 'KAMPFUMO', morada)
        dados['morada'] = morada

    return dados


def parsear_mrz(texto: str) -> dict:
    """Extrai dados da MRZ do BI moçambicano."""
    dados = {}
    linhas = texto.upper().split('\n')

    l1 = l2 = l3 = None
    for linha in linhas:
        limpa = re.sub(r'[^A-Z0-9<]', '', linha)
        if len(limpa) >= 28:
            if limpa.startswith('ID') or limpa.startswith('1D'):
                l1 = limpa
            elif re.match(r'^\d{6}[0-9][MF]\d{6}', limpa):
                l2 = limpa
            elif re.match(r'^[A-Z]{2,}<<', limpa):
                l3 = limpa

    if l2 and len(l2) >= 18:
        dob_raw = l2[0:6]
        sex     = l2[7:8]
        exp_raw = l2[8:14]
        yy_b = dob_raw[0:2]
        year_b = ('19' if int(yy_b) > 30 else '20') + yy_b
        dados['dataNascimento'] = f"{year_b}-{dob_raw[2:4]}-{dob_raw[4:6]}"
        dados['dataValidade']   = f"20{exp_raw[0:2]}-{exp_raw[2:4]}-{exp_raw[4:6]}"
        dados['sexo']           = sex if sex in ['M', 'F'] else None

    if l1 and len(l1) >= 14:
        dados['numeroBi'] = l1[5:14]

    if l3 and '<<' in l3:
        partes = l3.split('<<')
        apelido = partes[0]
        nomes = [n for n in partes[1].split('<') if n]
        dados['nomeCompleto'] = ' '.join([apelido] + nomes)

    return dados

def extrair_dados_ocr(msisdn: str) -> dict:
    """Corre OCR em frente e verso e combina dados."""
    pasta = ID_DIR / msisdn
    dados = {'ocrSucesso': False, 'ocrErros': []}

    for lado in ['bi_frente', 'bi_verso']:
        for ext in ['jpg', 'jpeg', 'png']:
            caminho = pasta / f"{lado}.{ext}"
            if caminho.exists():
                log(f"  OCR {lado}: {caminho}")
                texto = ocr_imagem(str(caminho))
                if texto:
                    if lado == 'bi_frente':
                        campos = parsear_frente(texto)
                        if campos:
                            dados.update(campos)
                            dados['ocrSucesso'] = True
                            log(f"  OCR {lado}: {len(campos)} campos extraídos da frente")
                    mrz = parsear_mrz(texto)
                    if mrz:
                        dados.update(mrz)
                        dados['ocrSucesso'] = True
                        log(f"  OCR {lado}: {len(mrz)} campos extraídos via MRZ")
                break

    return dados

# =============================================================================
# FACE MATCH
# =============================================================================

def extrair_face(caminho: str):
    """Extrai a maior face de uma imagem. Retorna array numpy ou None."""
    img = cv2.imread(caminho)
    if img is None:
        return None

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cascades = [
        cv2.data.haarcascades + 'haarcascade_frontalface_alt2.xml',
        cv2.data.haarcascades + 'haarcascade_frontalface_default.xml',
        cv2.data.haarcascades + 'haarcascade_frontalface_alt.xml',
    ]

    for cascade_path in cascades:
        cascade = cv2.CascadeClassifier(cascade_path)
        faces = cascade.detectMultiScale(gray, 1.1, 4, minSize=(25,25))
        if len(faces) > 0:
            faces = sorted(faces, key=lambda f: f[2]*f[3], reverse=True)
            x, y, w, h = faces[0]
            face = gray[y:y+h, x:x+w]
            return cv2.resize(face, (128, 128))

    return None

def comparar_faces(face1, face2) -> float:
    """Compara duas faces. Retorna score 0-100."""
    if face1 is None or face2 is None:
        return 0.0

    scores = {}

    # Histograma
    h1 = cv2.calcHist([face1], [0], None, [256], [0,256])
    h2 = cv2.calcHist([face2], [0], None, [256], [0,256])
    cv2.normalize(h1, h1); cv2.normalize(h2, h2)
    scores['hist_corr']  = max(0, cv2.compareHist(h1, h2, cv2.HISTCMP_CORREL)) * 100
    scores['bhatt']      = (1 - cv2.compareHist(h1, h2, cv2.HISTCMP_BHATTACHARYYA)) * 100

    # Template matching
    result = cv2.matchTemplate(face1.astype('float32'), face2.astype('float32'), cv2.TM_CCOEFF_NORMED)
    scores['template']   = max(0, float(result[0][0])) * 100

    # Pixel similarity
    diff = cv2.absdiff(face1, face2).astype('float32')
    scores['pixel']      = max(0, 100 - float((diff**2).mean()) / 2.55)

    weights = {'hist_corr': 0.35, 'bhatt': 0.25, 'template': 0.25, 'pixel': 0.15}
    return sum(weights[k] * v for k, v in scores.items())

def fazer_face_match(msisdn: str) -> dict:
    """Extrai faces do BI e da selfie e compara."""
    pasta  = ID_DIR / msisdn
    result = {'matchScore': 0.0, 'matchOk': False, 'matchErro': None}

    # Encontrar foto do BI com face
    face_bi = None
    for lado in ['bi_frente']:
        for ext in ['jpg', 'jpeg', 'png']:
            caminho = pasta / f"{lado}.{ext}"
            if caminho.exists():
                face_bi = extrair_face(str(caminho))
                if face_bi is not None:
                    log(f"  Face BI extraída de {caminho.name}")
                break

    if face_bi is None:
        result['matchErro'] = 'face_bi_nao_detectada'
        log(f"  AVISO: face não detectada no BI")
        return result

    # Selfie
    face_selfie = None
    for ext in ['jpg', 'jpeg', 'png']:
        caminho = pasta / f"selfie.{ext}"
        if caminho.exists():
            face_selfie = extrair_face(str(caminho))
            if face_selfie is not None:
                log(f"  Face selfie extraída de {caminho.name}")
            break

    if face_selfie is None:
        result['matchErro'] = 'face_selfie_nao_detectada'
        log(f"  AVISO: face não detectada na selfie")
        return result

    score = comparar_faces(face_bi, face_selfie)
    result['matchScore'] = round(score, 2)
    result['matchOk']    = score >= THRESHOLD_AUTO
    log(f"  Face match score: {score:.1f}% ({'MATCH' if result['matchOk'] else 'INCERTO/MISMATCH'})")
    return result

# =============================================================================
# PROCESSAMENTO PRINCIPAL
# =============================================================================

def processar(msisdn: str):
    log(f"=== PROCESSAR IDENTIFICAÇÃO: {msisdn} ===")

    conn   = conectar_db()
    cursor = conn.cursor()

    # Verificar estado actual
    cursor.execute("""
        SELECT revendedorId, estadoId, nomeCompleto
        FROM TblRevendedor WHERE msisdn=%s LIMIT 1
    """, (msisdn,))
    rev = cursor.fetchone()
    if not rev:
        log(f"  ERRO: revendedor não encontrado para {msisdn}")
        return

    revendedorId = rev['revendedorId']
    estadoId     = rev['estadoId']
    nome         = rev['nomeCompleto']
    log(f"  Revendedor: {nome} (id={revendedorId}, estado={estadoId})")

    # 1. OCR
    dados_ocr = extrair_dados_ocr(msisdn)

    # 2. Face match
    face_result = fazer_face_match(msisdn)

    # 3. Gravar em TblBIDados
    ocr_ok    = dados_ocr.get('ocrSucesso', False)
    match_ok  = face_result.get('matchOk', False)
    match_score = face_result.get('matchScore', 0.0)
    match_erro  = face_result.get('matchErro')

    cursor.execute("""
        INSERT INTO TblBIDados
            (revendedorId, numeroBi, nomeCompleto, dataNascimento,
             sexo, dataValidade, ocrConfianca, ocrMotor, createdAt)
        VALUES (%s,%s,%s,%s,%s,%s,%s,'tesseract+opencv',NOW())
        ON DUPLICATE KEY UPDATE
            numeroBi       = COALESCE(VALUES(numeroBi), numeroBi),
            nomeCompleto   = COALESCE(VALUES(nomeCompleto), nomeCompleto),
            dataNascimento = COALESCE(VALUES(dataNascimento), dataNascimento),
            dataValidade   = COALESCE(VALUES(dataValidade), dataValidade),
            ocrConfianca   = VALUES(ocrConfianca),
            updatedAt      = NOW()
    """, (
        revendedorId,
        dados_ocr.get('numeroBi'),
        dados_ocr.get('nomeCompleto'),
        dados_ocr.get('dataNascimento'),
        dados_ocr.get('sexo'),
        dados_ocr.get('dataValidade'),
        THRESHOLD_AUTO if ocr_ok else 0
    ))

    # 4. Decidir estado
    if ocr_ok and match_ok:
        novo_estado = ESTADO_VALIDADO
        resultado   = 'auto_aprovado'
        log(f"  RESULTADO: AUTO APROVADO → estadoId=5")
    else:
        novo_estado = ESTADO_VALIDACAO  # mantém em 4, revisão humana
        resultado   = 'revisao_humana'
        motivo = []
        if not ocr_ok:    motivo.append('ocr_falhou')
        if not match_ok:  motivo.append(f"face_score={match_score:.1f}%")
        if match_erro:    motivo.append(match_erro)
        log(f"  RESULTADO: REVISÃO HUMANA → {', '.join(motivo)}")

    # 5. Actualizar TblRevendedor — nome + numeroBi + estado
    nome_ocr    = dados_ocr.get('nomeCompleto')
    numero_bi   = dados_ocr.get('numeroBi')

    if nome_ocr and not nome_ocr.startswith('REV-'):
        cursor.execute("""
            UPDATE TblRevendedor
            SET estadoId     = %s,
                nomeCompleto = %s,
                numeroBi     = %s,
                updatedAt    = NOW()
            WHERE revendedorId = %s
        """, (novo_estado, nome_ocr, numero_bi, revendedorId))
        log(f"  Nome actualizado: {nome_ocr} | BI: {numero_bi}")
    else:
        cursor.execute("""
            UPDATE TblRevendedor
            SET estadoId  = %s,
                updatedAt = NOW()
            WHERE revendedorId = %s
        """, (novo_estado, revendedorId))

    # 6. Log em TblAdminAcao
    detalhe = json.dumps({
        'ocr_ok': ocr_ok,
        'face_score': match_score,
        'face_erro': match_erro,
        'resultado': resultado,
    })
    cursor.execute("""
        INSERT INTO TblAdminAcao (adminId, revendedorId, tipoAcao, detalhe, createdAt)
        VALUES (0, %s, %s, %s, NOW())
    """, (revendedorId, f'validacao_automatica_{resultado}', detalhe))

    conn.commit()
    cursor.close()
    conn.close()

    log(f"=== FIM: {msisdn} → estado={novo_estado} ===\n")
    return novo_estado

def processar_pendentes():
    """Processa todos os revendedores em estadoId=4 com fotos ainda não validadas."""
    conn   = conectar_db()
    cursor = conn.cursor()
    cursor.execute("""
        SELECT r.msisdn
        FROM TblRevendedor r
        LEFT JOIN TblBIDados b ON b.revendedorId = r.revendedorId
        WHERE r.estadoId = %s
          AND r.biFrente IS NOT NULL
          AND r.biVerso IS NOT NULL
          AND r.selfie IS NOT NULL
          AND (b.validadoManual = 0 OR b.biDadosId IS NULL)
        LIMIT 20
    """, (ESTADO_VALIDACAO,))
    rows = cursor.fetchall()
    cursor.close()
    conn.close()

    log(f"Pendentes a processar: {len(rows)}")
    for row in rows:
        try:
            processar(row['msisdn'])
        except Exception as e:
            log(f"ERRO ao processar {row['msisdn']}: {e}")

# =============================================================================
# ENTRY POINT
# =============================================================================

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--msisdn',    help='Processar número específico')
    parser.add_argument('--pendentes', action='store_true', help='Processar todos pendentes')
    args = parser.parse_args()

    if args.msisdn:
        processar(args.msisdn)
    elif args.pendentes:
        processar_pendentes()
    else:
        parser.print_help()
