# app/blueprints/bank/routes.py
"""
Загрузка и обработка банковских выписок (PDF-файлы).
Добавлены:
  • RBAC-декоратор @role_required.
  • rate limit на все POST-эндпоинты.
  • жёсткая валидация входящих данных (bank_key, INN, тип файла).
  • Path-sandbox – сохранение только во внутренний UPLOAD_FOLDER.
  • автоматическая очистка временных файлов после генерации.
"""

from __future__ import annotations

import logging
import os
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

from flask import (
    Blueprint,
    current_app,
    flash,
    jsonify,
    redirect,
    render_template,
    request,
    send_file,
    session,
    url_for,
)
from flask_limiter.util import get_remote_address
from werkzeug.utils import secure_filename

from app.database import get_db_connection
from app.extensions import limiter              # один общий Limiter, уже инициализирован
from app.security.rbac import role_required
from app.services.bank_statement_service import BankStatementService
from app.parsers.privatbank_pdf_parser import PrivatBankPdfParser
from app.parsers.taskombank_pdf_parser import TaskombankPdfParser
from app.parsers.mono_pdf_parser import MonobankPdfParser
from app.parsers.mtbbank_pdf_parser import MTBBankPdfParser
from app.parsers.ukrgasbank_pdf_parser import UkrgasbankPdfParser
from app.parsers.raiffeisen_pdf_parser import RaiffeisenBankPdfParser
from app.services.stats_service import StatsService
from app.parsers.sensebank_pdf_parser import SenseBankPdfParser

log = logging.getLogger(__name__)
bank_bp = Blueprint("bank_bp", __name__)

# ────────────────────────────────────────────────────────────────────
# Конфигурация / константы
# --------------------------------------------------------------------
ALLOWED_EXTENSIONS = {"pdf"}
# максимально 15 МБ на файл; общий лимит в Config.MAX_CONTENT_LENGTH
MAX_FILE_SIZE = 20 * 1024 * 1024

# зарегистрированные парсеры
service = BankStatementService()
service.register_parser("privat_pdf",     PrivatBankPdfParser)
service.register_parser("taskombank_pdf", TaskombankPdfParser)
service.register_parser("monobank_pdf",   MonobankPdfParser)
service.register_parser("mtbbank_pdf",    MTBBankPdfParser)
service.register_parser("ukrgasbank_pdf", UkrgasbankPdfParser)
service.register_parser("raiffeisen_pdf", RaiffeisenBankPdfParser)
service.register_parser("sensebank_pdf", SenseBankPdfParser)

# ────────────────────────────────────────────────────────────────────
# Вспомогательные функции
# --------------------------------------------------------------------
def _allowed_file(filename: str) -> bool:
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS


def _safe_path(dst_dir: Path, filename: str) -> Path:
    """
    Создаёт абсолютный путь вида <UPLOAD_FOLDER>/<uuid>.<ext>,
    гарантируя, что он остаётся внутри UPLOAD_FOLDER.
    """
    ext = filename.rsplit(".", 1)[1].lower()
    safe_name = f"{uuid.uuid4()}.{ext}"
    out = (dst_dir / safe_name).resolve()
    if dst_dir not in out.parents:
        # защитимся от path traversal на уровне ОС
        raise ValueError("Bad upload path")
    return out


def _fetch_user(user_id: int | None) -> dict[str, Any] | None:
    if not user_id:
        return None
    with get_db_connection() as conn, conn.cursor() as cur:
        cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        return cur.fetchone()


def _check_and_reset_parse_count(user: dict[str, Any]) -> dict[str, Any]:
    """
    Оновлюємо лічильник генерацій, якщо минуло >30 днів.
    """
    plan_limit = current_app.config["PLAN_LIMITS"].get(user["subscription_plan"])
    parse_reset_date = user.get("parse_reset_date")
    now = datetime.utcnow()

    if not parse_reset_date or parse_reset_date < now:
        new_count = 999_999 if plan_limit is None else plan_limit
        new_reset = now + timedelta(days=30)
        with get_db_connection() as conn, conn.cursor() as cur:
            cur.execute(
                "UPDATE users SET parse_count=%s, parse_reset_date=%s WHERE id=%s",
                (new_count, new_reset, user["id"]),
            )
            conn.commit()
        user["parse_count"] = new_count
        user["parse_reset_date"] = new_reset

    return user



def _current_user() -> tuple[dict[str, Any] | None, str | None]:
    """
    Повертає (user | None, error | None) з такою логікою:
      • Якщо user не знайдений → (None, "Ви не авторизовані")
      • Якщо role='disabled' → (None, "Учётна запис вимкнена")
      • Якщо role='admin' → (user, None)
      • Якщо is_pf_client=1:
          – Дістанемо pay_status із planfix_companies
          – Якщо pay_status ∉ {"Сплачений", "Постоплата"}:
                → повернути (user, "Оплата по договору відсутня")
          – Інакше (user, None)
        (Під PF-клієнтом не знімаємо лічильник parse_count, не дивимося payment_date)
      • Інакше (звичайний зовнішній клієнт):
          – user = _check_and_reset_parse_count(user)
          – Якщо payment_date прострочена → повернути (user, "Оплата прострочена")
          – Якщо ліміт генерацій ≤ 0 → повернути (user, "Ліміт генерацій вичерпано")
          – Інакше → (user, None)
    """
    user = _fetch_user(session.get("user_id"))
    if not user:
        return None, "Ви не авторизовані"
    if user["role"] == "disabled":
        return None, "Учётна запис вимкнена"

    # 1) Адміністратор завжди ОК
    if user["role"] == "admin":
        return user, None

    # 2) PF-клієнт (із договором PlanFix)
    if user.get("is_pf_client"):
        pf_id = user.get("pf_company_id")
        if not pf_id:
            return None, "Не вказано договір PlanFix"

        with get_db_connection() as conn, conn.cursor() as cur:
            cur.execute(
                "SELECT pay_status FROM planfix_companies WHERE pf_company_id=%s",
                (pf_id,)
            )
            row = cur.fetchone()

        if not row:
            return None, "Договір не знайдено у PlanFix-кеші"

        # Якщо статус НЕ “Сплачений” і НЕ “Постоплата”, повертаємо самого user + повідомлення
        if row["pay_status"] not in ("Сплачений", "Постоплата"):
            return user, "Оплата по договору відсутня"

        # Якщо статус валідний — повертаємо без помилки (без зняття ліміту)
        return user, None

    # 3) Звичайний (зовнішній) клієнт
    user = _check_and_reset_parse_count(user)
    today = datetime.utcnow().date()

    if not user["payment_date"] or user["payment_date"] < today:
        # Повертаємо user + повідомлення, щоб він залишився на сторінці
        return user, "Оплата прострочена"

    limit = current_app.config["PLAN_LIMITS"].get(user["subscription_plan"])
    if limit is not None and user["parse_count"] <= 0:
        return user, "Ліміт генерацій вичерпано"

    return user, None



# ────────────────────────────────────────────────────────────────────
# Роуты
# --------------------------------------------------------------------
@bank_bp.get("/status")
@role_required("client", "admin")
def bank_status():
    user, err = _current_user()
    if err:
        return jsonify({"status": "error", "message": err}), 403

    unlimited = user["is_pf_client"] or user["subscription_plan"] == "pro_plus"
    count = "∞" if unlimited else user["parse_count"]
    return jsonify({"status": "OK", "parse_count": count})


@bank_bp.get("/")
@role_required("client", "admin")
def bank_main():
    user, err = _current_user()
    if user is None:
        flash(err or "Помилка авторизації", "danger")
        return redirect(url_for("auth_bp.login"))

    plan            = "admin" if user["role"] == "admin" else user["subscription_plan"]
    is_pf_client    = bool(user.get("is_pf_client"))
    is_unlimited    = is_pf_client or plan == "pro_plus"
    left_count      = user["parse_count"] if not is_unlimited else "Безліміт"
    payment_date    = user.get("payment_date")
    available_banks = [b.strip() for b in user.get("available_banks", "").split(",") if b.strip()]
    disable_actions = (err is not None) and (user["role"] != "admin")

    return render_template(
        "parsing_landing.html",
        subscription_plan   = plan,
        parse_count         = left_count,
        user_role           = user["role"],
        all_banks           = current_app.config["ALL_BANKS"],
        available_banks_list= available_banks,
        payment_date        = payment_date,
        today               = datetime.utcnow().date(),
        is_pf_client        = is_pf_client,
        is_unlimited_plan   = is_unlimited,
        disable_actions     = disable_actions,
        error_msg           = err,
    )



# --- UPLOAD ---------------------------------------------------------
@bank_bp.post("/upload")
@limiter.limit("15/minute", key_func=get_remote_address)
@role_required("client", "admin")
def bank_upload():
    user, err = _current_user()
    if err:
        # Якщо PF- або зовнішній клієнт має помилку, вертаємо 403 і не обробляємо файли
        return jsonify({"status": "error", "message": err}), 403

    bank_key = request.form.get("bank_key", "").strip()
    if bank_key not in current_app.config["ALL_BANKS"]:
        return jsonify({"status": "error", "message": "Невірний банк"}), 400

    payer_inn = request.form.get("payer_inn", "").strip()
    if not payer_inn.isdigit():
        return jsonify({"status": "error", "message": "ІПН повинен містити лише цифри"}), 400

    upload_dir = Path(current_app.config["UPLOAD_FOLDER"])
    upload_dir.mkdir(parents=True, exist_ok=True)

    added: list[dict[str, str]] = []
    for f in request.files.getlist("pdf_files"):
        if not f or not f.filename:
            continue
        if not _allowed_file(f.filename):
            return jsonify({"status": "error", "message": "Дозволені лише PDF-файли"}), 400
        if f.content_length and f.content_length > MAX_FILE_SIZE:
            return jsonify({"status": "error", "message": "Файл занадто великий"}), 400

        dst_path = _safe_path(upload_dir, secure_filename(f.filename))
        f.save(dst_path)
        log.debug("Saved upload to %s", dst_path)

        added.append({
            "id": dst_path.stem,
            "originalName": f.filename,
            "file_path": str(dst_path),
            "bank_key": bank_key,
            "payer_inn": payer_inn,
            "sender": request.form.get("sender", "ФОП"),
        })
        StatsService.log(user["id"], "upload", f.filename)

    session.setdefault("upload_list", []).extend(added)
    return jsonify({
        "status": "OK",
        "files": [{k: v for k, v in x.items() if k in ("id", "originalName")} for x in session["upload_list"]]
    })


# --- DELETE ---------------------------------------------------------
@bank_bp.post("/delete")
@limiter.limit("30/minute", key_func=get_remote_address)
@role_required("client", "admin")
def bank_delete():
    user, err = _current_user()
    if err:
        return jsonify({"status": "error", "message": err}), 403

    file_id = request.form.get("file_id", "")
    if not file_id:
        return jsonify({"status": "error", "message": "file_id необхідний"}), 400

    new_list = []
    for item in session.get("upload_list", []):
        if item["id"] == file_id:
            try:
                Path(item["file_path"]).unlink(missing_ok=True)
            except OSError as exc:
                log.warning("Can't delete file %s: %s", item["file_path"], exc)
        else:
            new_list.append(item)
    session["upload_list"] = new_list
    files_meta = [{"id": x["id"], "originalName": x["originalName"]} for x in new_list]
    return jsonify({"status": "OK", "files": files_meta})


# --- GENERATE -------------------------------------------------------
@bank_bp.post("/generate")
@limiter.limit("10/minute", key_func=get_remote_address)
@role_required("client", "admin")
def bank_generate():
    user, err = _current_user()
    if err:
        return jsonify({"status": "error", "message": err}), 403

    unlimited = user["is_pf_client"] or user["subscription_plan"] == "pro_plus"
    if (user["role"] != "admin") and (not unlimited) and (user["parse_count"] <= 0):
        return jsonify({"status": "error", "message": "Ліміт генерацій вичерпано"}), 403

    upload_list: list[dict[str, Any]] = session.get("upload_list", [])
    if not upload_list:
        return jsonify({"status": "error", "message": "Немає завантажених файлів"}), 400

    result_parts: list[str] = []
    for item in upload_list:
        path = Path(item["file_path"])
        if not path.exists():
            continue
        try:
            result_parts.append(
                service.process_file(
                    str(path),
                    item["bank_key"],
                    item["payer_inn"],
                    item["sender"],
                ).strip()
            )
        except Exception as exc:
            log.exception("Parser error")
            return jsonify({"status": "error", "message": str(exc)}), 500

    combined_text = "\n".join(result_parts)
    if not combined_text.endswith("КонецФайла"):
        combined_text += "\nКонецФайла"

    # Зберігаємо результат у текстовий файл
    upload_dir = Path(current_app.config["UPLOAD_FOLDER"])
    out_path = _safe_path(upload_dir, "result.txt")
    try:
        out_path.write_text(combined_text, encoding="cp1251")
    except Exception as exc:
        log.exception("Write error")
        return jsonify({"status": "error", "message": str(exc)}), 500

    if (user["role"] != "admin") and (not unlimited) and not user.get("is_pf_client"):
        with get_db_connection() as conn, conn.cursor() as cur:
            cur.execute(
                "UPDATE users "
                "SET parse_count = GREATEST(parse_count - 1, 0) "
                "WHERE id=%s",
                (user["id"],),
            )
            conn.commit()

    # Очищуємо тимчасові файли завантаження
    for itm in upload_list:
        try:
            Path(itm["file_path"]).unlink(missing_ok=True)
        except OSError:
            pass

    session["upload_list"] = []
    session["last_generated_file"] = str(out_path)
    StatsService.log(user["id"], "generate", out_path.name)
    return send_file(out_path, as_attachment=True, download_name="result_out.txt")



# --- LAST FILE ------------------------------------------------------
@bank_bp.get("/last_file")
@role_required("client", "admin")
def bank_last_file():
    user, err = _current_user()
    if err:
        flash(err, "danger")
        return redirect(url_for("auth_bp.login"))

    out_path = Path(session.get("last_generated_file", ""))
    if not out_path.exists():
        flash("Нет последнего файла или файл не найден", "warning")
        return redirect(url_for("bank_bp.bank_main"))

    return send_file(out_path, as_attachment=True, download_name="result_out.txt")
