#!/usr/bin/env python3
"""
Calcula a Matriz RFV (Recencia, Frequencia, Valor) de uma base de clientes.

Le um CSV com colunas de cliente, data da ultima compra, numero de compras e
total gasto. Atribui nota 1-5 para R, F e V, classifica cada cliente em um dos
11 segmentos do Metodo Imperatriz e gera dois arquivos:
  - <saida>_clientes.csv  -> base original + colunas R, F, V, codigo, segmento
  - <saida>_resumo.json   -> contagem, receita e percentual por segmento (alimenta o dashboard)

Usa apenas a biblioteca padrao (csv, json, datetime, argparse). Nao importa XML,
entao roda mesmo no Python do Homebrew com pyexpat quebrado. Se preferir: `uv run`.

Dois modos de pontuacao:
  --metodo quintil  (padrao) -> divide a base em 5 partes iguais por percentil.
                                 Melhor para bases >= 100 clientes.
  --metodo faixa             -> usa faixas fixas por ticket (ver --ticket).
                                 Melhor para bases pequenas (< 100 clientes).

Exemplos:
  python3 calcular_rfv.py base.csv --saida rfv
  python3 calcular_rfv.py base.csv --metodo faixa --ticket high --saida rfv
  python3 calcular_rfv.py base.csv --col-id email --col-data ultima --col-freq pedidos --col-valor ltv
"""
import argparse
import csv
import json
import sys
from datetime import date, datetime

# ---------------------------------------------------------------------------
# Faixas fixas por tipo de ticket (modo --metodo faixa).
# Cada faixa e (nota, limite_superior). Recencia: dias (quanto MENOR, melhor).
# Frequencia: nº de compras. Valor: R$ total gasto.
# ---------------------------------------------------------------------------
FAIXAS = {
    "low": {  # produto de entrada / baixo ticket (ate ~R$200)
        "R": [(5, 30), (4, 90), (3, 180), (2, 365), (1, float("inf"))],
        "F": [(1, 1), (2, 2), (3, 4), (4, 7), (5, float("inf"))],
        "V": [(1, 99), (2, 299), (3, 699), (4, 1499), (5, float("inf"))],
    },
    "mid": {  # ticket medio (R$200 a R$2000)
        "R": [(5, 30), (4, 90), (3, 180), (2, 365), (1, float("inf"))],
        "F": [(1, 1), (2, 2), (3, 3), (4, 5), (5, float("inf"))],
        "V": [(1, 299), (2, 799), (3, 1999), (4, 4999), (5, float("inf"))],
    },
    "high": {  # high ticket (mentoria, R$2000+)
        "R": [(5, 60), (4, 180), (3, 365), (2, 540), (1, float("inf"))],
        "F": [(1, 1), (2, 1), (3, 2), (4, 3), (5, float("inf"))],
        "V": [(1, 1999), (2, 4999), (3, 9999), (4, 19999), (5, float("inf"))],
    },
}

# Datas aceitas para a coluna de ultima compra.
DATE_FORMATS = ["%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y", "%Y/%m/%d", "%m/%d/%Y"]


def parse_date(value):
    value = (value or "").strip()
    if not value:
        return None
    for fmt in DATE_FORMATS:
        try:
            return datetime.strptime(value, fmt).date()
        except ValueError:
            continue
    return None


def parse_number(value):
    if value is None:
        return 0.0
    s = str(value).strip().replace("R$", "").replace(" ", "")
    if not s:
        return 0.0
    # Trata formatos BR (1.234,56) e US (1,234.56).
    if "," in s and "." in s:
        if s.rfind(",") > s.rfind("."):
            s = s.replace(".", "").replace(",", ".")
        else:
            s = s.replace(",", "")
    elif "," in s:
        s = s.replace(",", ".")
    try:
        return float(s)
    except ValueError:
        return 0.0


def nota_por_faixa(valor, faixas):
    for nota, limite in faixas:
        if valor <= limite:
            return nota
    return faixas[-1][0]


def nota_por_quintil(valor, ordenados, inverso=False):
    """Nota 1-5 pela posicao percentil de `valor` dentro de `ordenados` (asc)."""
    n = len(ordenados)
    if n == 0:
        return 3
    # posicao = quantos valores sao <= valor
    pos = sum(1 for v in ordenados if v <= valor)
    pct = pos / n
    nota = min(5, int(pct * 5) + 1)
    if inverso:  # recencia: menos dias = nota maior
        nota = 6 - nota
    return max(1, min(5, nota))


# ---------------------------------------------------------------------------
# Classificacao em segmentos. Recebe notas R, F, V (1-5) e devolve o nome do
# segmento do Metodo Imperatriz. Ordem importa: regras mais especificas primeiro.
# ---------------------------------------------------------------------------
def classificar(r, f, v):
    fv = (f + v) / 2  # forca de "lealdade + valor"
    if r >= 4 and f >= 4 and v >= 4:
        return "Campeoes"
    if r >= 3 and fv >= 4:
        return "Clientes Fieis"
    if r >= 4 and fv >= 2.5:
        return "Fieis em Potencial"
    if r == 5 and f <= 2:
        return "Novos Clientes"
    if r >= 4 and fv < 2.5:
        return "Promissores"
    if r == 1 and fv >= 4.5:
        return "Nao Posso Perde-los"
    if r <= 2 and fv >= 3.5:
        return "Em Risco"
    if r == 3 and fv >= 2.5:
        return "Precisam de Atencao"
    if r in (2, 3) and fv >= 2:
        return "Prestes a Dormir"
    if r <= 2 and fv >= 1.5:
        return "Hibernando"
    return "Perdidos"


def main():
    ap = argparse.ArgumentParser(description="Calcula a Matriz RFV de uma base de clientes.")
    ap.add_argument("entrada", help="CSV de entrada")
    ap.add_argument("--saida", default="rfv", help="prefixo dos arquivos de saida (default: rfv)")
    ap.add_argument("--metodo", choices=["quintil", "faixa"], default="quintil")
    ap.add_argument("--ticket", choices=["low", "mid", "high"], default="mid",
                    help="faixa de ticket para --metodo faixa (default: mid)")
    ap.add_argument("--hoje", default=None, help="data de referencia YYYY-MM-DD (default: hoje)")
    ap.add_argument("--col-id", default="cliente")
    ap.add_argument("--col-data", default="data_ultima_compra")
    ap.add_argument("--col-freq", default="qtd_compras")
    ap.add_argument("--col-valor", default="total_gasto")
    ap.add_argument("--delimiter", default=None, help="delimitador do CSV (auto-detecta , ou ;)")
    args = ap.parse_args()

    hoje = parse_date(args.hoje) if args.hoje else date.today()
    if hoje is None:
        print(f"Data --hoje invalida: {args.hoje}", file=sys.stderr)
        sys.exit(1)

    # Leitura + deteccao de delimitador.
    with open(args.entrada, newline="", encoding="utf-8-sig") as fh:
        sample = fh.read(4096)
        fh.seek(0)
        delim = args.delimiter or (";" if sample.count(";") > sample.count(",") else ",")
        reader = csv.DictReader(fh, delimiter=delim)
        rows = list(reader)
        fieldnames = reader.fieldnames or []

    if not rows:
        print("CSV vazio ou sem linhas de dados.", file=sys.stderr)
        sys.exit(1)

    for col in (args.col_id, args.col_data, args.col_freq, args.col_valor):
        if col not in fieldnames:
            print(f"Coluna '{col}' nao encontrada. Colunas disponiveis: {fieldnames}", file=sys.stderr)
            print("Use --col-id / --col-data / --col-freq / --col-valor para mapear.", file=sys.stderr)
            sys.exit(1)

    # Extrai metricas brutas.
    clientes = []
    for row in rows:
        dt = parse_date(row.get(args.col_data))
        recencia = (hoje - dt).days if dt else 99999
        freq = parse_number(row.get(args.col_freq))
        valor = parse_number(row.get(args.col_valor))
        clientes.append({"row": row, "recencia": recencia, "freq": freq, "valor": valor})

    # Pontuacao.
    if args.metodo == "quintil":
        rec_ord = sorted(c["recencia"] for c in clientes)
        freq_ord = sorted(c["freq"] for c in clientes)
        val_ord = sorted(c["valor"] for c in clientes)
        for c in clientes:
            c["R"] = nota_por_quintil(c["recencia"], rec_ord, inverso=True)
            c["F"] = nota_por_quintil(c["freq"], freq_ord)
            c["V"] = nota_por_quintil(c["valor"], val_ord)
    else:
        fx = FAIXAS[args.ticket]
        for c in clientes:
            c["R"] = nota_por_faixa(c["recencia"], fx["R"])
            c["F"] = nota_por_faixa(c["freq"], fx["F"])
            c["V"] = nota_por_faixa(c["valor"], fx["V"])

    for c in clientes:
        c["codigo"] = f"{c['R']}{c['F']}{c['V']}"
        c["segmento"] = classificar(c["R"], c["F"], c["V"])

    # Saida 1: CSV enriquecido.
    out_csv = f"{args.saida}_clientes.csv"
    extra = ["R", "F", "V", "codigo_rfv", "segmento"]
    with open(out_csv, "w", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=fieldnames + extra)
        writer.writeheader()
        for c in clientes:
            r = dict(c["row"])
            r.update({"R": c["R"], "F": c["F"], "V": c["V"],
                      "codigo_rfv": c["codigo"], "segmento": c["segmento"]})
            writer.writerow(r)

    # Saida 2: resumo por segmento.
    total = len(clientes)
    receita_total = sum(c["valor"] for c in clientes)
    seg = {}
    for c in clientes:
        s = seg.setdefault(c["segmento"], {"clientes": 0, "receita": 0.0})
        s["clientes"] += 1
        s["receita"] += c["valor"]
    resumo = {
        "gerado_em": hoje.isoformat(),
        "metodo": args.metodo,
        "ticket": args.ticket if args.metodo == "faixa" else None,
        "total_clientes": total,
        "receita_total": round(receita_total, 2),
        "segmentos": [
            {
                "segmento": nome,
                "clientes": d["clientes"],
                "pct_clientes": round(100 * d["clientes"] / total, 1),
                "receita": round(d["receita"], 2),
                "pct_receita": round(100 * d["receita"] / receita_total, 1) if receita_total else 0,
            }
            for nome, d in sorted(seg.items(), key=lambda kv: -kv[1]["receita"])
        ],
    }
    out_json = f"{args.saida}_resumo.json"
    with open(out_json, "w", encoding="utf-8") as fh:
        json.dump(resumo, fh, ensure_ascii=False, indent=2)

    # Relatorio no terminal.
    print(f"OK -> {out_csv} ({total} clientes) | {out_json}")
    print(f"Receita total: R$ {receita_total:,.2f}\n")
    print(f"{'Segmento':<22}{'Clientes':>9}{'% base':>8}{'Receita R$':>15}{'% receita':>10}")
    for s in resumo["segmentos"]:
        print(f"{s['segmento']:<22}{s['clientes']:>9}{s['pct_clientes']:>7.1f}%"
              f"{s['receita']:>15,.2f}{s['pct_receita']:>9.1f}%")


if __name__ == "__main__":
    main()
