#!/usr/bin/env python3
"""
PhotographerManager — Scraper vendite (multi-struttura)
========================================================
Estrae le vendite da tutte le strutture disponibili e salva in CSV.

Architettura:
  - Playwright  → login + auto-discovery strutture (richiede JavaScript)
  - requests    → download dati per ogni struttura (veloce)
  - pandas      → parsing HTML e salvataggio CSV

Utilizzo
--------
  # Estrazione intera stagione 2025 (tutte le strutture):
  python3 pm_scraper.py --mode full --year 2025

  # Solo una struttura specifica:
  python3 pm_scraper.py --mode full --year 2025 --struttura Marinabeach1

  # Delta giornaliero tutte le strutture (per cron):
  python3 pm_scraper.py --mode delta

Credenziali
-----------
  Crea un file .env nella stessa cartella con:
    PM_EMAIL=tua_email@esempio.com
    PM_PASSWORD=tua_password

Dipendenze
----------
  pip3 install requests beautifulsoup4 pandas lxml python-dotenv playwright
  python3 -m playwright install chromium
"""

import os
import sys
import argparse
import logging
from datetime import date, timedelta
from pathlib import Path

import requests
import pandas as pd
from bs4 import BeautifulSoup

try:
    from dotenv import load_dotenv
    load_dotenv()
except ImportError:
    pass

# ── Logging ────────────────────────────────────────────────────────────────────
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger(__name__)

# ── Configurazione ─────────────────────────────────────────────────────────────
BASE_URL      = "https://app.photographermanager.com"
LOGIN_URL     = f"{BASE_URL}/login"
DASHBOARD_URL = f"{BASE_URL}/village-db"
DATA_URL      = f"{BASE_URL}/village-dettaglio"

# Account 1 (principale)
EMAIL    = os.getenv("PM_EMAIL", "")
PASSWORD = os.getenv("PM_PASSWORD", "")

# Account 2 (opzionale — seconda società)
EMAIL2    = os.getenv("PM_EMAIL2", "")
PASSWORD2 = os.getenv("PM_PASSWORD2", "")

def get_accounts() -> list[tuple[str, str]]:
    """Restituisce la lista di account configurati in .env."""
    accounts = []
    if EMAIL and PASSWORD:
        accounts.append((EMAIL, PASSWORD))
    if EMAIL2 and PASSWORD2:
        accounts.append((EMAIL2, PASSWORD2))
    if not accounts:
        raise RuntimeError(
            "Nessuna credenziale trovata. "
            "Imposta PM_EMAIL e PM_PASSWORD nel file .env"
        )
    return accounts

DETAIL_COLS = {"Data Vendita", "Fotografo", "Formato", "Camera", "Quantità", "Prezzo"}

OUTPUT_DIR = Path(__file__).parent


# ── Login con Playwright ───────────────────────────────────────────────────────

def login_and_get_cookies(email: str, password: str) -> dict:
    """Login headless e restituzione cookies per requests."""
    from playwright.sync_api import sync_playwright

    log.info(f"Avvio browser headless per il login ({email})...")
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent=(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/120.0.0.0 Safari/537.36"
            )
        )
        page = context.new_page()

        log.info("Navigazione alla pagina di login...")
        page.goto(LOGIN_URL, wait_until="networkidle", timeout=30000)

        log.info("Inserimento credenziali e login...")
        page.fill('input[name="email"]', email)
        page.fill('input[name="password"]', password)

        with page.expect_navigation(wait_until="networkidle", timeout=30000):
            page.click('input[type="submit"]')

        if "login" in page.url.lower():
            raise RuntimeError(
                f"Login fallito per {email}. Verifica email e password nel file .env."
            )

        log.info(f"Login riuscito → {page.url}")
        cookies = {c["name"]: c["value"] for c in context.cookies()}
        browser.close()

    return cookies


# ── Auto-discovery strutture ───────────────────────────────────────────────────

def discover_strutture(session: requests.Session) -> list[str]:
    """
    Legge la dashboard e restituisce la lista di tutte le strutture disponibili.
    Fallback alla lista manuale se la pagina cambia struttura.
    """
    log.info("Ricerca strutture disponibili...")
    r = session.get(DASHBOARD_URL, timeout=30)
    r.raise_for_status()

    if "login" in r.url.lower():
        raise RuntimeError("Sessione scaduta durante il discovery delle strutture.")

    soup = BeautifulSoup(r.text, "lxml")

    strutture = []

    # Pattern 1: titoli delle card struttura (h2, h3, h4 con nome struttura)
    for tag in soup.find_all(["h2", "h3", "h4", "h5"]):
        text = tag.get_text(strip=True)
        if text and len(text) > 2 and len(text) < 50:
            strutture.append(text)

    # Pattern 2: link che contengono il parametro struttura
    for a in soup.find_all("a", href=True):
        href = a["href"]
        if "struttura=" in href:
            nome = href.split("struttura=")[1].split("&")[0]
            if nome and nome not in strutture:
                strutture.append(nome)

    # Deduplicazione mantenendo l'ordine
    seen = set()
    result = []
    for s in strutture:
        if s not in seen:
            seen.add(s)
            result.append(s)

    if result:
        log.info(f"Strutture trovate: {result}")
        return result

    # Fallback: lista dalla URL che conosciamo già
    log.warning(
        "Auto-discovery non ha trovato strutture. "
        "Usa --struttura per specificarla manualmente."
    )
    return []


# ── Sessione requests ──────────────────────────────────────────────────────────

def create_session(cookies: dict) -> requests.Session:
    s = requests.Session()
    s.headers.update({
        "User-Agent": (
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/120.0.0.0 Safari/537.36"
        ),
        "Referer": BASE_URL,
    })
    for name, value in cookies.items():
        s.cookies.set(name, value, domain="app.photographermanager.com")
    return s


# ── Estrazione dati ────────────────────────────────────────────────────────────

def fetch_vendite(
    session: requests.Session,
    struttura: str,
    data1: str,
    data2: str,
    fotografo: str = "all",
) -> pd.DataFrame:
    params = {
        "struttura": struttura,
        "data1":     data1,
        "data2":     data2,
        "fotografo": fotografo,
    }
    log.info(f"  [{struttura}] Scarico {data1} → {data2}...")
    r = session.get(DATA_URL, params=params, timeout=60)
    r.raise_for_status()

    if "login" in r.url.lower():
        raise RuntimeError(f"Sessione scaduta durante il download di {struttura}.")

    try:
        from io import StringIO
        tables = pd.read_html(StringIO(r.text), flavor="lxml")
    except ValueError:
        log.warning(f"  [{struttura}] Nessuna tabella trovata.")
        return pd.DataFrame()

    for df in tables:
        cols = set(df.columns.astype(str))
        if DETAIL_COLS.issubset(cols):
            df = _clean_df(df)
            df.insert(0, "Struttura", struttura)   # aggiunge colonna struttura
            log.info(f"  [{struttura}] ✓ {len(df)} righe estratte.")
            return df

    log.warning(f"  [{struttura}] Colonne vendite non trovate — fallback bs4.")
    return _extract_with_bs4(r.text, struttura)


def _clean_df(df: pd.DataFrame) -> pd.DataFrame:
    df = df.dropna(how="all").drop_duplicates()
    if "Data Vendita" in df.columns:
        df["Data Vendita"] = pd.to_datetime(
            df["Data Vendita"], dayfirst=True, errors="coerce"
        ).dt.date
    return df.reset_index(drop=True)


def _extract_with_bs4(html: str, struttura: str) -> pd.DataFrame:
    soup = BeautifulSoup(html, "lxml")
    for table in soup.find_all("table"):
        headers = [th.get_text(strip=True) for th in table.find_all("th")]
        if any(col in headers for col in ["Data Vendita", "Fotografo"]):
            rows = []
            tbody = table.find("tbody")
            if tbody:
                for tr in tbody.find_all("tr"):
                    cells = [td.get_text(strip=True) for td in tr.find_all("td")]
                    if cells:
                        rows.append(cells)
            if rows:
                df = pd.DataFrame(rows, columns=headers[:len(rows[0])])
                df = _clean_df(df)
                df.insert(0, "Struttura", struttura)
                return df
    return pd.DataFrame()


# ── Salvataggio ───────────────────────────────────────────────────────────────

def save_csv(df: pd.DataFrame, path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    df.to_csv(path, index=False, encoding="utf-8-sig")
    log.info(f"  Salvato: {path.name} ({len(df)} righe)")


# ── Modalità full ─────────────────────────────────────────────────────────────

def mode_full(strutture: list[str], year: int) -> None:
    """Estrae l'intera stagione per ogni struttura, su tutti gli account."""
    accounts = get_accounts()

    for i, (email, password) in enumerate(accounts, 1):
        log.info(f"\n{'='*50}")
        log.info(f"Account {i}/{len(accounts)}: {email}")
        log.info(f"{'='*50}")

        cookies = login_and_get_cookies(email, password)
        session = create_session(cookies)

        strutture_account = strutture or discover_strutture(session)
        if not strutture_account:
            log.warning(f"Nessuna struttura trovata per {email}. Salto.")
            continue

        log.info(f"Estrazione stagione {year} — {len(strutture_account)} strutture")

        for struttura in strutture_account:
            df = fetch_vendite(session, struttura, f"{year}-01-01", f"{year}-12-31")
            if not df.empty:
                out = OUTPUT_DIR / f"vendite_{struttura}_{year}.csv"
                save_csv(df, out)

    log.info(f"\n✓ Estrazione completata per tutti gli account.")


# ── Modalità delta ────────────────────────────────────────────────────────────

def mode_delta(strutture: list[str], days_back: int = 1) -> None:
    """Estrae i record recenti per ogni struttura e aggiorna i CSV, su tutti gli account."""
    today     = date.today()
    data_from = today - timedelta(days=days_back)

    accounts = get_accounts()

    for i, (email, password) in enumerate(accounts, 1):
        log.info(f"\n{'='*50}")
        log.info(f"Account {i}/{len(accounts)}: {email}")
        log.info(f"Delta {data_from} → {today}")
        log.info(f"{'='*50}")

        cookies = login_and_get_cookies(email, password)
        session = create_session(cookies)

        strutture_account = strutture or discover_strutture(session)
        if not strutture_account:
            log.warning(f"Nessuna struttura trovata per {email}. Salto.")
            continue

        for struttura in strutture_account:
            df_new = fetch_vendite(session, struttura, str(data_from), str(today))
            if df_new.empty:
                log.info(f"  [{struttura}] Nessun nuovo record.")
                continue

            out = OUTPUT_DIR / f"vendite_{struttura}_{today.year}.csv"
            if out.exists():
                df_existing = pd.read_csv(out, encoding="utf-8-sig")
                df_combined = (
                    pd.concat([df_existing, df_new], ignore_index=True)
                    .drop_duplicates()
                    .reset_index(drop=True)
                )
                new_rows = len(df_combined) - len(df_existing)
                save_csv(df_combined, out)
                log.info(f"  [{struttura}] +{new_rows} nuove righe.")
            else:
                save_csv(df_new, out)

    log.info("\n✓ Delta completato.")


# ── Entry point ───────────────────────────────────────────────────────────────

def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="PhotographerManager — scraper vendite multi-struttura"
    )
    p.add_argument("--mode",      choices=["full", "delta"], required=True)
    p.add_argument("--struttura", nargs="+", default=[],
                   help="Una o più strutture (default: tutte)")
    p.add_argument("--year",      type=int, default=date.today().year)
    p.add_argument("--days-back", type=int, default=1, dest="days_back")
    return p.parse_args()


if __name__ == "__main__":
    args = parse_args()
    try:
        if args.mode == "full":
            mode_full(args.struttura, args.year)
        elif args.mode == "delta":
            mode_delta(args.struttura, args.days_back)
    except RuntimeError as e:
        log.error(str(e))
        sys.exit(1)
    except KeyboardInterrupt:
        log.info("Interrotto dall'utente.")
        sys.exit(0)
