import re
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Any, Optional
import math

DATE_RE = re.compile(r"\b\d{2}/\d{2}/\d{4}\b")

AMOUNT_RE = re.compile(
    r"(?<!\w)(\d{1,3}(?:[ .]\d{3})*(?:[.,]\d{2})|\d+(?:[.,]\d{2}))(?!\w)"
)

HEADER_BLACKLIST = [
    "processado por computador",
    "www.",
    "dados de cliente",
    "dados da conta",
    "account data",
    "customer data",
    "saldo inicial",
    "saldo final",
]

# Keywords for risk detection (Portuguese and English)
KEYWORDS = {
    "cheque_devolvido": [r"\bdevolv", r"\binsufici[eê]ncia", r"\bsem fundos", r"\bcheque devolvido"],
    "saque": [r"\bsaque\b", r"\blevantamento\b", r"\bmultibanco\b", r"\batm\b", r"\bcash withdrawal\b"],
    "transf_risco": [r"\bjogo\b", r"\baposta\b", r"\bcasino\b", r"\bbet", r"\bforex\b", r"\bc[âa]mbio\b", r"\boffshore\b", r"\bbingo\b", r"\bloteria\b"],
    "deposito_dinheiro": [r"\bdep[óo]sito numer[áa]rio\b", r"\bdinheiro\b", r"\bcash deposit\b", r"\bdep[óo]sito em esp[ée]cie\b"],
}

def _normalize_amount(raw: str) -> float:
    raw = raw.replace(" ", "")
    if "." in raw and "," in raw:
        raw = raw.replace(".", "").replace(",", ".")
    elif "," in raw:
        raw = raw.replace(",", ".")
    try:
        return float(raw)
    except ValueError:
        return 0.0

def _is_noise_block(text: str) -> bool:
    text = text.lower()
    return any(k in text for k in HEADER_BLACKLIST)

def _count_keywords(text: str, patterns: List[str]) -> int:
    """Count occurrences of any of the patterns in text (case-insensitive)."""
    count = 0
    for pat in patterns:
        count += len(re.findall(pat, text, re.IGNORECASE))
    return count

def _std_dev(values: List[float]) -> float:
    """Compute sample standard deviation."""
    if len(values) < 2:
        return 0.0
    mean = sum(values) / len(values)
    variance = sum((x - mean) ** 2 for x in values) / (len(values) - 1)
    return math.sqrt(variance)

def analyze_bank_statement(text: str) -> Dict[str, Any]:
    """
    Análise de extrato bancário com regras heurísticas detalhadas.
    Retorna sempre Dict[str, Any] com floats normais.
    """
    try:
        return _analyze_bank_statement_impl(text)
    except Exception as e:
        return _insufficient(reasons=[f"(Info) Erro na análise: {str(e)}"])

def _analyze_bank_statement_impl(text: str) -> Dict[str, Any]:
    # ---------- Extração de blocos por data ----------
    matches = list(DATE_RE.finditer(text))
    if len(matches) < 2:
        return _insufficient()

    blocks: List[Tuple[datetime, str]] = []
    for i in range(len(matches)):
        start = matches[i].start()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
        try:
            date = datetime.strptime(matches[i].group(), "%d/%m/%Y")
        except ValueError:
            continue
        block = text[start:end].strip()
        blocks.append((date, block))

    # ---------- Extração de saldos por bloco ----------
    records: List[Tuple[datetime, float]] = []  # (data, saldo)
    for date, block in blocks:
        if _is_noise_block(block):
            continue
        raw_amounts = AMOUNT_RE.findall(block)
        amounts = [_normalize_amount(a) for a in raw_amounts if _normalize_amount(a) > 0]
        if len(amounts) < 2:
            continue
        saldo = max(amounts)  # assume that the largest number is the balance
        if saldo <= 0:
            continue
        records.append((date, saldo))

    if len(records) < 2:
        return _insufficient(valid=len(records))

    records.sort(key=lambda x: x[0])
    first_date = records[0][0]
    last_date = records[-1][0]
    period_days = (last_date - first_date).days + 1

    # ---------- Construir série diária de saldos ----------
    daily_balance = {}
    current_balance = records[0][1]
    current_date = records[0][0]
    idx = 1
    for day in range(period_days):
        day_date = first_date + timedelta(days=day)
        if idx < len(records) and records[idx][0] == day_date:
            current_balance = records[idx][1]
            idx += 1
        daily_balance[day_date] = current_balance

    # ---------- Métricas base ----------
    total_creditos = 0.0
    total_debitos = 0.0
    dias_com_delta_positivo = 0
    prev = records[0][1]
    daily_net_debits = []  # list of net debit amounts on days with negative delta
    for i in range(1, len(records)):
        delta = records[i][1] - prev
        if delta > 0:
            total_creditos += delta
            dias_com_delta_positivo += 1
        elif delta < 0:
            total_debitos += abs(delta)
            daily_net_debits.append(abs(delta))
        prev = records[i][1]

    fluxo_liquido = total_creditos - total_debitos
    saldos = [r[1] for r in records]
    saldo_medio = sum(saldos) / len(saldos)
    saldo_minimo = min(saldos)

    period_months = max(period_days / 30.0, 1.0 / 30.0)
    media_entradas_mensal = total_creditos / period_months
    media_saidas_mensal = total_debitos / period_months

    # ---------- Novas métricas para regras heurísticas ----------
    negative_days = sum(1 for bal in daily_balance.values() if bal < 0)
    min_balance = saldo_minimo

    keyword_counts = {}
    for key, patterns in KEYWORDS.items():
        keyword_counts[key] = _count_keywords(text, patterns)

    if daily_net_debits:
        avg_daily_debit = sum(daily_net_debits) / len(daily_net_debits)
        max_daily_debit = max(daily_net_debits)
        concentration_flag = max_daily_debit > 5 * avg_daily_debit
    else:
        concentration_flag = False
        avg_daily_debit = 0

    daily_vals = list(daily_balance.values())
    std_daily = _std_dev(daily_vals)
    volatility_ratio = std_daily / saldo_medio if saldo_medio > 0 else 0

    first_balance = records[0][1]
    last_balance = records[-1][1]
    balance_change_pct = (last_balance - first_balance) / first_balance if first_balance != 0 else 0
    decline_flag = balance_change_pct < -0.3

    all_numbers = AMOUNT_RE.findall(text)
    large_numbers = [n for n in all_numbers if _normalize_amount(n) > 1000]
    if large_numbers:
        integer_count = sum(1 for n in large_numbers if _normalize_amount(n).is_integer())
        round_proportion = integer_count / len(large_numbers)
    else:
        round_proportion = 0
    round_flag = round_proportion > 0.5

    fluxo_positivo = total_creditos > total_debitos
    growth_flag = balance_change_pct > 0.1

    months_with_positive = set()
    for i in range(1, len(records)):
        if records[i][1] > records[i-1][1]:
            months_with_positive.add(records[i][0].strftime("%Y-%m"))
    revenue_regular = len(months_with_positive) >= 2

    # ---------- Cálculo do score heurístico ----------
    score = 50

    # Penalidades
    score -= min(negative_days, 5)
    if min_balance < 0:
        score -= 3
    elif 0 < min_balance < 0.1 * media_saidas_mensal:
        score -= 2
    score -= min(keyword_counts.get("cheque_devolvido", 0) * 5, 10)
    score -= min(keyword_counts.get("saque", 0) * 1, 5)
    score -= min(keyword_counts.get("transf_risco", 0) * 2, 6)
    if concentration_flag:
        score -= 3
    if volatility_ratio > 2:
        score -= 4
    if decline_flag:
        score -= 5
    if round_flag:
        score -= 2
    score -= min(keyword_counts.get("deposito_dinheiro", 0) * 1, 4)

    # Bónus
    if fluxo_positivo:
        if fluxo_liquido > 500000:
            score += 10
        else:
            score += 5
    score += min(int(saldo_medio / 100000), 20)
    if growth_flag:
        if balance_change_pct > 0.5:
            score += 6
        else:
            score += 3
    if revenue_regular:
        score += 4

    statement_score = max(0, min(100, int(score)))

    # ---------- Limite recomendado ----------
    fluxo_liquido_mensal = fluxo_liquido / period_months
    if statement_score >= 60:
        base_limit_30 = 0.5 * fluxo_liquido_mensal
    elif statement_score >= 30:
        base_limit_30 = 0.3 * fluxo_liquido_mensal
    else:
        base_limit_30 = 0.0

    recommended_max_by_term = {
        15: max(0.0, round(base_limit_30 * 0.5, 2)),
        30: max(0.0, round(base_limit_30, 2)),
        45: max(0.0, round(base_limit_30 * 1.5, 2)),
    }

    # ---------- Critérios antigos (para compatibilidade) ----------
    CC = dias_com_delta_positivo / period_days if period_days > 0 else 0.0
    entradas_maior_saidas = media_entradas_mensal > media_saidas_mensal
    if entradas_maior_saidas and CC >= 0.6:
        c1_points, c1_label = 40, "Alta"
    elif entradas_maior_saidas and CC >= 0.4:
        c1_points, c1_label = 25, "Média"
    elif entradas_maior_saidas and CC >= 0.3:
        c1_points, c1_label = 10, "Baixa"
    else:
        c1_points, c1_label = 0, "Reprovado"

    criterion_1 = {
        "points": c1_points,
        "label": c1_label,
        "details": {
            "CC": round(CC, 4),
            "entradas_maior_saidas": entradas_maior_saidas,
            "media_entradas_mensal": round(media_entradas_mensal, 2),
            "media_saidas_mensal": round(media_saidas_mensal, 2),
        },
    }

    MSR = saldo_medio / media_saidas_mensal if media_saidas_mensal > 0 else 0.0
    estresse = sum(1 for s in saldos if s < 0.10 * saldo_medio)
    if MSR >= 3.0 and estresse == 0:
        c2_points, c2_label = 35, "Forte"
    elif MSR >= 1.5 and estresse <= 1:
        c2_points, c2_label = 25, "Aceitável"
    elif MSR >= 0.5 and estresse <= 3:
        c2_points, c2_label = 10, "Fraca"
    else:
        c2_points, c2_label = 0, "Reprovado"

    criterion_2 = {
        "points": c2_points,
        "label": c2_label,
        "details": {
            "MSR": round(MSR, 4),
            "estresse": estresse,
            "saldo_medio": round(saldo_medio, 2),
            "media_saidas_mensal": round(media_saidas_mensal, 2),
        },
    }

    CPL_mensal = media_entradas_mensal - media_saidas_mensal
    c3_points = 25 if CPL_mensal > 0 else 0
    criterion_3 = {
        "points": c3_points,
        "label": "Aprovado" if c3_points > 0 else "Reprovado",
        "details": {
            "CPL_mensal": round(CPL_mensal, 2),
            "recommended_max_by_term": recommended_max_by_term,
        },
    }

    # ---------- Status e confiança ----------
    records_count = len(records)
    if records_count >= 10 and period_days >= 25:
        analysis_status = "OK"
    else:
        analysis_status = "INSUFFICIENT_DATA"

    if period_days >= 60 and records_count >= 30:
        confidence = 0.9
    elif analysis_status == "OK":
        confidence = 0.7
    elif period_days >= 15 and period_days < 25:
        confidence = 0.4
    else:
        confidence = 0.2

    has_valid_extrato = (analysis_status == "OK" and records_count > 0)

    # ---------- Reasons ----------
    reasons = []
    if analysis_status == "OK":
        reasons.append("(Pos) Período e quantidade de registros suficientes para análise.")
    else:
        reasons.append("(Neg) Dados insuficientes (poucos registros ou período curto).")

    if fluxo_liquido > 0:
        reasons.append("(Pos) Fluxo líquido positivo no período.")
    else:
        reasons.append("(Neg) Fluxo líquido não positivo.")

    reasons.append(f"(Info) Período analisado: {period_days} dias, {records_count} registros.")
    reasons.append(f"(Info) Score do extrato: {statement_score}/100 (baseado em regras heurísticas).")

    return {
        "analysis_status": analysis_status,
        "confidence": round(confidence, 2),
        "period_days": int(period_days),
        "records_count": int(records_count),
        "total_creditos": round(total_creditos, 2),
        "total_debitos": round(total_debitos, 2),
        "fluxo_liquido": round(fluxo_liquido, 2),
        "saldo_medio": round(saldo_medio, 2),
        "saldo_minimo": round(saldo_minimo, 2),
        "media_entradas_mensal": round(media_entradas_mensal, 2),
        "media_saidas_mensal": round(media_saidas_mensal, 2),
        "criterion_1": criterion_1,
        "criterion_2": criterion_2,
        "criterion_3": criterion_3,
        "statement_score": statement_score,
        "recommended_max_by_term": recommended_max_by_term,
        "recommended_limits": recommended_max_by_term,  # sinônimo para compatibilidade
        "has_valid_extrato": has_valid_extrato,
        "reasons": reasons,
    }

def _insufficient(
    valid: int = 0,
    reasons: Optional[List[str]] = None,
) -> Dict[str, Any]:
    r = reasons or []
    if not r:
        r.append("(Neg) Dados insuficientes para análise do extrato.")
    return {
        "analysis_status": "INSUFFICIENT_DATA",
        "confidence": 0.2,
        "period_days": 0,
        "records_count": int(valid),
        "total_creditos": 0.0,
        "total_debitos": 0.0,
        "fluxo_liquido": 0.0,
        "saldo_medio": 0.0,
        "saldo_minimo": 0.0,
        "media_entradas_mensal": 0.0,
        "media_saidas_mensal": 0.0,
        "criterion_1": {"points": 0, "label": "Reprovado", "details": {}},
        "criterion_2": {"points": 0, "label": "Reprovado", "details": {}},
        "criterion_3": {"points": 0, "label": "Reprovado", "details": {}},
        "statement_score": 0,
        "recommended_max_by_term": {15: 0.0, 30: 0.0, 45: 0.0},
        "recommended_limits": {15: 0.0, 30: 0.0, 45: 0.0},
        "has_valid_extrato": False,
        "reasons": r,
    }