"""
Receipt handlers for Telegram bot - OCR fiscal receipt flow.

Handles: PDF/JPG document messages, photo messages, and receipt callback buttons.
Flow: File received → OCR → Preview → Confirm/Cancel → Oracle write.
"""
import asyncio
import logging
import re
import tempfile
from datetime import date, datetime
from pathlib import Path
from typing import Optional

import sqlite3

import oracledb
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.error import TelegramError
from telegram.ext import ContextTypes
from telegram.constants import ParseMode

from backend.modules.telegram.db.operations import get_user, is_user_linked
from backend.modules.data_entry.services.ocr.queue_client import submit_ocr_job, wait_for_result
from backend.modules.data_entry.services.oracle_receipt_writer import write_receipt
from backend.config import settings
from shared.database.oracle_pool import oracle_pool

logger = logging.getLogger(__name__)

# ============================================================================
# MODULE CONSTANTS
# ============================================================================

OCR_TIMEOUT_S = 120
OCR_POLL_INTERVAL_S = 1.0
PENDING_TTL_S = 600
LOW_CONFIDENCE_THRESHOLD = 0.60
TEMP_FILE_PREFIX = 'receipt_'

# ============================================================================
# MODULE-LEVEL CACHES
# ============================================================================

# Pending receipts: user_id -> {receipt_dict, temp_path, created_at}
_pending_receipts: dict[int, dict] = {}

# Track which write pools have been registered in oracle_pool
_write_pool_registered: set[str] = set()


# ============================================================================
# PRIVATE HELPERS
# ============================================================================

def _is_pending_expired(pending: dict) -> bool:
    return (datetime.now() - pending['created_at']).total_seconds() > PENDING_TTL_S


def _cleanup_expired_pending() -> None:
    """Remove expired pending receipts and their temp files."""
    expired = [uid for uid, p in _pending_receipts.items() if _is_pending_expired(p)]
    for uid in expired:
        entry = _pending_receipts.pop(uid)
        try:
            Path(entry['temp_path']).unlink(missing_ok=True)
        except OSError:
            pass


def _normalize_receipt_date(receipt_dict: dict) -> None:
    """Parse receipt_date in-place; fallback to today if None or malformed."""
    raw = receipt_dict.get("receipt_date")
    try:
        if raw is None:
            receipt_dict["receipt_date"] = datetime.now().date()
        elif isinstance(raw, datetime):
            receipt_dict["receipt_date"] = raw.date()
        elif isinstance(raw, date):
            pass  # already correct
        elif isinstance(raw, str):
            receipt_dict["receipt_date"] = date.fromisoformat(raw)
        else:
            receipt_dict["receipt_date"] = datetime.now().date()
    except (ValueError, TypeError):
        receipt_dict["receipt_date"] = datetime.now().date()


def _build_oracle_config(server_id: Optional[str]) -> Optional[dict]:
    """Build Oracle connection config dict for a given server_id.

    Single Oracle user per server: read and write share credentials.
    """
    srv = (
        settings.get_oracle_server(server_id)
        if server_id
        else settings.get_default_oracle_server()
    )
    if not srv:
        return None

    return {
        "user": srv.user,
        "password": srv.password,
        "dsn": srv.get_dsn(),
    }


def _format_receipt_preview(receipt_dict: dict) -> str:
    """Format receipt data as a Telegram-friendly Markdown preview."""
    partner = receipt_dict.get("partner_name") or "Necunoscut"
    cui = receipt_dict.get("cui") or "—"
    dt = receipt_dict.get("receipt_date")
    date_str = dt.strftime("%d.%m.%Y") if isinstance(dt, (date, datetime)) else str(dt or "—")
    amount = float(receipt_dict.get("amount") or 0)
    tva = float(receipt_dict.get("tva_total") or 0)
    nr = receipt_dict.get("receipt_number") or "—"

    return (
        "📄 *Preview bon fiscal*\n\n"
        f"🏢 *Furnizor:* {partner}\n"
        f"🔢 *CUI:* {cui}\n"
        f"📅 *Data:* {date_str}\n"
        f"🔖 *Nr. bon:* {nr}\n"
        f"💰 *Total:* {amount:.2f} RON\n"
        f"🧾 *TVA:* {tva:.2f} RON\n"
    )


def _confidence_warning(confidence: Optional[float]) -> str:
    """Return a warning string when OCR confidence is below threshold."""
    if confidence is not None and confidence < LOW_CONFIDENCE_THRESHOLD:
        return f"\n⚠ *Atenție:* Calitate OCR scăzută ({confidence:.0%}) — verificați datele!"
    return ""


def _format_oracle_error(e: oracledb.DatabaseError) -> str:
    """Translate common ORA error codes to user-friendly Romanian messages."""
    msg = str(e)
    if "ORA-01017" in msg:
        return "❌ Credențiale Oracle incorecte. Contactați administratorul."
    if "ORA-00001" in msg:
        return "❌ Bon duplicat — documentul există deja în sistem."
    if "ORA-12541" in msg:
        return "❌ Serverul Oracle nu este disponibil. Încercați mai târziu."
    return f"❌ Eroare Oracle: {msg}"


# Oracle identifier rule: starts with letter/underscore, rest letters/digits/underscore.
# We uppercase before matching since schemas are stored uppercase in v_nom_firme.
_SCHEMA_RE = re.compile(r"^[A-Z_][A-Z0-9_]*$")


def _sync_write_with_pool(
    receipt_dict: dict,
    pool: oracledb.ConnectionPool,
) -> tuple[int, str]:
    """Acquire a connection from the pool and write the receipt synchronously."""
    conn = pool.acquire()
    try:
        return write_receipt(receipt_dict, conn)
    finally:
        conn.close()


async def _resolve_schema(server_id: Optional[str], company_id: int) -> Optional[str]:
    """Look up Oracle schema for company_id via CONTAFIN_ORACLE.v_nom_firme."""
    try:
        async with oracle_pool.get_connection(server_id) as conn:
            with conn.cursor() as cur:
                cur.execute(
                    "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :id",
                    id=company_id,
                )
                row = cur.fetchone()
                if not row:
                    return None
                schema = (row[0] or "").upper().strip()
                if not _SCHEMA_RE.match(schema):
                    logger.warning("Invalid schema name from v_nom_firme: %r", schema)
                    return None
                return schema
    except Exception as e:
        logger.warning("Schema lookup failed for company %s: %s", company_id, e)
        return None


async def _save_to_oracle(
    receipt_dict: dict,
    oracle_cfg: dict,
    server_id: Optional[str] = None,
    schema: Optional[str] = None,
) -> tuple[int, str]:
    """Write receipt to Oracle as the schema owner (one Oracle user per schema).

    PACK_CONTAFIN's dynamic SQL uses SESSION_USER (not CURRENT_SCHEMA), so we must
    connect AS the schema owner. We assume the schema-owner Oracle user shares the
    read user's password (configured per-server in secrets/<id>.oracle_pass).
    """
    if schema:
        # Per-(server, schema) pool: each schema is a separate Oracle user.
        pool_key = f"{server_id or 'default'}_write_{schema}"

        if pool_key not in _write_pool_registered:
            srv = (
                settings.get_oracle_server(server_id)
                if server_id
                else settings.get_default_oracle_server()
            )
            if srv:
                oracle_pool.register_server(
                    pool_key,
                    host=srv.host,
                    port=srv.port,
                    user=schema,                       # connect AS schema owner
                    password=oracle_cfg["password"],   # same password as read user
                    sid=srv.sid,
                    service_name=srv.service_name,
                    min_connections=1,
                    max_connections=3,
                )
                _write_pool_registered.add(pool_key)

        if oracle_pool.is_server_registered(pool_key):
            pool = await oracle_pool.get_pool(pool_key)
            return await asyncio.to_thread(_sync_write_with_pool, receipt_dict, pool)

    # Fallback: direct connection with the configured user (will likely fail
    # for receipt writes that need schema-owner privileges, but used by tests).
    return await asyncio.to_thread(write_receipt, receipt_dict, oracle_cfg)


async def _submit_ocr_and_preview(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
    temp_path: Path,
    user_id: int,
) -> None:
    """Submit file to OCR queue, wait for result, then show confirm/cancel preview."""
    processing_msg = await update.message.reply_text(
        "⏳ Procesez bonul fiscal... (poate dura până la 2 minute)",
        parse_mode=ParseMode.MARKDOWN,
    )

    try:
        job_id = await submit_ocr_job(temp_path)
    except (OSError, sqlite3.Error) as e:
        logger.error("OCR job submission failed for user %s: %s", user_id, e)
        await processing_msg.edit_text(
            "❌ Eroare internă la procesarea bonului.",
            parse_mode=ParseMode.MARKDOWN,
        )
        temp_path.unlink(missing_ok=True)
        return

    try:
        result = await asyncio.wait_for(
            wait_for_result(job_id, timeout=OCR_TIMEOUT_S),
            timeout=OCR_TIMEOUT_S + 5,
        )
    except asyncio.TimeoutError:
        await processing_msg.edit_text(
            "⏱ Timeout OCR — bonul nu a fost procesat la timp. Încercați din nou.",
            parse_mode=ParseMode.MARKDOWN,
        )
        temp_path.unlink(missing_ok=True)
        return

    if not result.get("success"):
        error = result.get("error", "Eroare necunoscută")
        await processing_msg.edit_text(
            f"❌ OCR eșuat: {error}",
            parse_mode=ParseMode.MARKDOWN,
        )
        temp_path.unlink(missing_ok=True)
        return

    receipt_dict = result["result"] or {}
    _normalize_receipt_date(receipt_dict)
    confidence = receipt_dict.get("confidence")

    _pending_receipts[user_id] = {
        "receipt_dict": receipt_dict,
        "temp_path": str(temp_path),
        "created_at": datetime.now(),
    }

    preview = _format_receipt_preview(receipt_dict)
    warning = _confidence_warning(confidence)
    keyboard = InlineKeyboardMarkup([
        [
            InlineKeyboardButton("✅ Confirmă", callback_data=f"receipt:confirm:{user_id}"),
            InlineKeyboardButton("❌ Anulează", callback_data=f"receipt:cancel:{user_id}"),
        ]
    ])

    try:
        await processing_msg.edit_text(
            preview + warning + "\n\nConfirmați salvarea în Oracle?",
            parse_mode=ParseMode.MARKDOWN,
            reply_markup=keyboard,
        )
    except TelegramError as e:
        logger.error("Failed to send OCR preview for user %s: %s", user_id, e)
        _pending_receipts.pop(user_id, None)
        temp_path.unlink(missing_ok=True)


# ============================================================================
# PUBLIC HANDLERS
# ============================================================================

async def handle_document_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle incoming PDF or image document messages."""
    user_id = update.effective_user.id

    if not await is_user_linked(user_id):
        await update.message.reply_text(
            "❗ Cont neconectat. Folosiți /login pentru a vă autentifica.",
            parse_mode=ParseMode.MARKDOWN,
        )
        return

    _cleanup_expired_pending()

    if user_id in _pending_receipts:
        await update.message.reply_text(
            "⚠ Ai un bon în așteptare. Confirmați sau anulați bonul anterior înainte de a trimite altul.",
            parse_mode=ParseMode.MARKDOWN,
        )
        return

    doc = update.message.document
    suffix = Path(doc.file_name or "receipt.pdf").suffix or ".pdf"

    tmp = tempfile.NamedTemporaryFile(prefix=TEMP_FILE_PREFIX, suffix=suffix, delete=False)
    temp_path = Path(tmp.name)
    tmp.close()

    try:
        tg_file = await context.bot.get_file(doc.file_id)
        await tg_file.download_to_drive(temp_path)
    except OSError as e:
        logger.error("Failed to download document for user %s: %s", user_id, e)
        await update.message.reply_text("❌ Nu am putut descărca fișierul.")
        temp_path.unlink(missing_ok=True)
        return

    await _submit_ocr_and_preview(update, context, temp_path, user_id)


async def handle_photo_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle incoming photo messages."""
    user_id = update.effective_user.id

    if not await is_user_linked(user_id):
        await update.message.reply_text(
            "❗ Cont neconectat. Folosiți /login pentru a vă autentifica.",
            parse_mode=ParseMode.MARKDOWN,
        )
        return

    _cleanup_expired_pending()

    if user_id in _pending_receipts:
        await update.message.reply_text(
            "⚠ Ai un bon în așteptare. Confirmați sau anulați bonul anterior înainte de a trimite altul.",
            parse_mode=ParseMode.MARKDOWN,
        )
        return

    photo = update.message.photo[-1]  # largest available resolution

    tmp = tempfile.NamedTemporaryFile(prefix=TEMP_FILE_PREFIX, suffix=".jpg", delete=False)
    temp_path = Path(tmp.name)
    tmp.close()

    try:
        tg_file = await context.bot.get_file(photo.file_id)
        await tg_file.download_to_drive(temp_path)
    except OSError as e:
        logger.error("Failed to download photo for user %s: %s", user_id, e)
        await update.message.reply_text("❌ Nu am putut descărca fotografia.")
        temp_path.unlink(missing_ok=True)
        return

    await _submit_ocr_and_preview(update, context, temp_path, user_id)


async def handle_receipt_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle receipt confirm/cancel callback queries (pattern: ^receipt:)."""
    query = update.callback_query
    await query.answer()

    parts = (query.data or "").split(":")
    if len(parts) != 3:
        await query.edit_message_text("❌ Cerere invalidă.")
        return

    _, action, target_uid_str = parts
    try:
        target_uid = int(target_uid_str)
    except ValueError:
        await query.edit_message_text("❌ Cerere invalidă.")
        return

    caller_uid = update.effective_user.id
    if caller_uid != target_uid:
        await query.answer("Nu ai permisiunea să acționezi pe bonul altcuiva.", show_alert=True)
        return

    pending = _pending_receipts.get(target_uid)
    if not pending:
        await query.edit_message_text("ℹ Bonul a expirat sau a fost deja procesat.")
        return

    if _is_pending_expired(pending):
        _pending_receipts.pop(target_uid, None)
        try:
            Path(pending['temp_path']).unlink(missing_ok=True)
        except OSError:
            pass
        await query.edit_message_text("⌛ Sesiune expirată. Retrimiteți bonul.")
        return

    if action == "cancel":
        _pending_receipts.pop(target_uid, None)
        try:
            Path(pending['temp_path']).unlink(missing_ok=True)
        except OSError:
            pass
        await query.edit_message_text("✖ Bon anulat. Fișierul a fost șters.")
        return

    if action != "confirm":
        await query.edit_message_text("❌ Acțiune necunoscută.")
        return

    # Confirm: build oracle config and write receipt
    user_data = await get_user(target_uid)
    if not user_data:
        await query.edit_message_text("❌ Utilizatorul nu a fost găsit.")
        return

    server_id = user_data.get("oracle_server_id")
    oracle_cfg = _build_oracle_config(server_id)
    if not oracle_cfg:
        await query.edit_message_text(
            "❌ Configurație Oracle lipsă. Contactați administratorul."
        )
        return

    # Resolve schema from the user's active company so dynamic SQL inside
    # PACK_CONTAFIN resolves table names against the correct schema.
    from backend.modules.telegram.agent.session import get_session_manager
    session = await get_session_manager().get_or_create_session(target_uid)
    company = session.get_active_company()
    if not company or not company.get("id"):
        await query.edit_message_text(
            "❌ Selectează mai întâi o firmă cu /companies."
        )
        return

    schema = await _resolve_schema(server_id, int(company["id"]))
    if not schema:
        await query.edit_message_text(
            f"❌ Schema firmei *{company.get('name') or company['id']}* nu a putut fi determinată.",
            parse_mode=ParseMode.MARKDOWN,
        )
        return

    await query.edit_message_text("⏳ Salvez în Oracle...")

    receipt_dict = pending["receipt_dict"]
    temp_path_str = pending["temp_path"]

    try:
        cod, mesaj = await _save_to_oracle(receipt_dict, oracle_cfg, server_id, schema)

        _pending_receipts.pop(target_uid, None)
        try:
            Path(temp_path_str).unlink(missing_ok=True)
        except OSError:
            pass

        logger.info("Receipt saved: user=%s cod=%s mesaj=%r", target_uid, cod, mesaj)
        await query.edit_message_text(
            f"✅ Bon salvat! Cod document: *{cod}*\n_{mesaj}_",
            parse_mode=ParseMode.MARKDOWN,
        )

    except oracledb.DatabaseError as e:
        logger.error("Oracle write error for user %s: %s", target_uid, e)
        await query.edit_message_text(
            _format_oracle_error(e),
            parse_mode=ParseMode.MARKDOWN,
        )

    except asyncio.TimeoutError:
        logger.error("Oracle write timeout for user %s", target_uid)
        await query.edit_message_text(
            "⏱ Timeout la scriere Oracle. Încercați din nou.",
            parse_mode=ParseMode.MARKDOWN,
        )
