"""
PDF Statement Import — Stories 9.1–9.6.

Flow:
  GET/POST /import/           → upload PDF
  GET      /import/review/<id> → staged review page
  POST     /import/update/<staged_id> → inline edit a staged row
  POST     /import/commit/<id> → commit to main DB
  POST     /import/reject/<staged_id> → reject a staged row
  GET      /import/history    → past import batches
"""
import json
import os
import tempfile
from datetime import datetime
from decimal import Decimal, InvalidOperation

from flask import render_template, redirect, url_for, flash, request, abort

from app.blueprints.import_pdf import import_pdf_bp
from app.blueprints.import_pdf.forms import UploadForm, ReviewActionForm
from app.extensions import db
from app.models.import_batch import ImportBatch
from app.models.account import Account
from app.models.category import Category
from app.models.merchant_mapping import MerchantMapping
from app.services.pdf_parsers.detector import detect_issuer, needs_confirmation, SUPPORTED_ISSUERS
from app.services import staging_pipeline
from app.services.csv_parser import parse_csv
from app.services.merchant_normalizer import normalize as normalize_merchant


def _parse_pdf(pdf_path: str, issuer: str):
    """Dispatch to the correct PDF parser. Returns (staged_txns, parse_errors)."""
    from app.services.pdf_parsers.base import ParseError
    try:
        if issuer == "chase":
            from app.services.pdf_parsers.chase import parse
        elif issuer == "bofa":
            from app.services.pdf_parsers.bofa import parse
        elif issuer == "discover":
            from app.services.pdf_parsers.discover import parse
        elif issuer == "kohls":
            from app.services.pdf_parsers.kohls import parse
        elif issuer == "apple_card":
            from app.services.pdf_parsers.apple import parse
        elif issuer == "target":
            from app.services.pdf_parsers.target import parse
        elif issuer == "dcu":
            from app.services.pdf_parsers.dcu import parse
        else:
            return [], [ParseError(0, "", f"No parser for issuer: {issuer}", "0.0")]
        return parse(pdf_path)
    except ImportError:
        return [], [ParseError(0, "", f"Parser for {issuer} not yet implemented.", "0.0")]
    except Exception as exc:
        return [], [ParseError(0, str(exc), "Unexpected parser error", "0.0")]


def _is_csv_file(filename: str) -> bool:
    return filename.lower().endswith((".csv", ".tsv", ".txt"))


# ── upload ─────────────────────────────────────────────────────────────────────

@import_pdf_bp.route("/", methods=["GET", "POST"])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        upload_file = form.pdf_file.data
        filename = upload_file.filename
        issuer_override = (form.issuer_override.data or "").strip()

        # ── CSV branch ────────────────────────────────────────────────────────
        if _is_csv_file(filename):
            issuer = issuer_override or "csv_import"
            csv_result = parse_csv(upload_file, issuer=issuer)
            staged_txns = csv_result.transactions
            parse_errors = csv_result.errors

        # ── PDF branch ────────────────────────────────────────────────────────
        else:
            suffix = os.path.splitext(filename)[1] or ".pdf"
            with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
                upload_file.save(tmp.name)
                tmp_path = tmp.name

            try:
                if issuer_override:
                    issuer = issuer_override
                else:
                    try:
                        import pdfplumber
                        with pdfplumber.open(tmp_path) as pdf:
                            first_page_text = pdf.pages[0].extract_text() or "" if pdf.pages else ""
                    except Exception:
                        first_page_text = ""
                    issuer, confidence = detect_issuer(first_page_text)
                    if issuer is None or needs_confirmation(confidence):
                        os.unlink(tmp_path)
                        flash(
                            "Could not detect the issuer automatically. Please select it manually.",
                            "warning",
                        )
                        return render_template(
                            "import_pdf/upload.html", form=form, active_page="import_pdf"
                        )

                staged_txns, parse_errors = _parse_pdf(tmp_path, issuer)
            finally:
                if os.path.exists(tmp_path):
                    os.unlink(tmp_path)

        errors_data = [
            {"page_number": e.page_number, "raw_text": e.raw_text[:200],
             "reason": e.reason, "parser_version": e.parser_version}
            for e in parse_errors
        ]
        batch = ImportBatch(
            filename=filename,
            issuer=issuer,
            status="pending",
            parse_errors_json=json.dumps(errors_data) if errors_data else None,
        )
        db.session.add(batch)
        db.session.commit()

        if staged_txns:
            result = staging_pipeline.stage_transactions(
                staged_txns, parse_errors, batch.id, issuer
            )
            batch.duplicates_skipped = result["duplicates_flagged"]
            db.session.commit()
            flash(
                f"Parsed {result['staged']} transactions "
                f"({result['duplicates_flagged']} possible duplicates). "
                "Review before committing.",
                "success",
            )
            return redirect(url_for("import_pdf.review", batch_id=batch.id))

        msg = (f"{len(parse_errors)} parse error(s)." if parse_errors
               else "No transactions found.")
        flash(msg, "warning" if parse_errors else "info")
        return redirect(url_for("import_pdf.history"))

    return render_template("import_pdf/upload.html", form=form, active_page="import_pdf")


# ── review ─────────────────────────────────────────────────────────────────────

@import_pdf_bp.route("/review/<int:batch_id>")
def review(batch_id):
    batch = ImportBatch.query.get_or_404(batch_id)
    if batch.status == "committed":
        flash("This import has already been committed.", "info")
        return redirect(url_for("import_pdf.history"))

    staged_rows = staging_pipeline.get_staged_transactions(
        batch_id, include_duplicates=True
    )
    categories = Category.query.filter_by(is_active=True).order_by(Category.name).all()
    accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
    parse_errors = _load_parse_errors(batch)
    action_form = ReviewActionForm()

    return render_template(
        "import_pdf/review.html",
        batch=batch,
        staged_rows=staged_rows,
        categories=categories,
        accounts=accounts,
        parse_errors=parse_errors,
        action_form=action_form,
        issuer_label=SUPPORTED_ISSUERS.get(batch.issuer or "", batch.issuer or "Unknown"),
        active_page="import_pdf",
    )


@import_pdf_bp.route("/update/<int:staged_id>", methods=["POST"])
def update_staged(staged_id):
    action_form = ReviewActionForm()
    if not action_form.validate_on_submit():
        abort(400)

    batch_id = request.form.get("batch_id", type=int)
    merchant = (request.form.get("merchant_normalized") or "").strip() or None
    category_id = request.form.get("category_id", type=int) or None
    account_id = request.form.get("account_id", type=int) or None
    raw_amount = (request.form.get("amount") or "").strip()

    amount = None
    if raw_amount:
        try:
            amount = Decimal(raw_amount)
        except InvalidOperation:
            flash("Invalid amount.", "error")
            return redirect(url_for("import_pdf.review", batch_id=batch_id))

    staging_pipeline.update_staged_transaction(
        staged_id,
        merchant_normalized=merchant,
        amount=amount,
        category_id=category_id,
        account_id=account_id,
    )
    if merchant and category_id:
        _save_merchant_mapping(merchant, category_id)

    flash("Row updated.", "success")
    return redirect(url_for("import_pdf.review", batch_id=batch_id))


@import_pdf_bp.route("/reject/<int:staged_id>", methods=["POST"])
def reject_staged(staged_id):
    action_form = ReviewActionForm()
    if not action_form.validate_on_submit():
        abort(400)
    batch_id = request.form.get("batch_id", type=int)
    staging_pipeline.update_staged_transaction(staged_id, status="rejected")
    flash("Row excluded.", "success")
    return redirect(url_for("import_pdf.review", batch_id=batch_id))


# ── commit ─────────────────────────────────────────────────────────────────────

@import_pdf_bp.route("/commit/<int:batch_id>", methods=["POST"])
def commit(batch_id):
    action_form = ReviewActionForm()
    if not action_form.validate_on_submit():
        abort(400)

    batch = ImportBatch.query.get_or_404(batch_id)
    if batch.status == "committed":
        flash("Already committed.", "info")
        return redirect(url_for("import_pdf.history"))

    default_acct = Account.query.filter_by(is_active=True).first()
    if not default_acct:
        flash("No accounts exist. Please add an account first.", "error")
        return redirect(url_for("import_pdf.review", batch_id=batch_id))

    count = staging_pipeline.commit_staged(batch_id, default_acct.id)
    flash(f"Committed {count} transactions.", "success")
    return redirect(url_for("import_pdf.history"))


# ── history ────────────────────────────────────────────────────────────────────

@import_pdf_bp.route("/history")
def history():
    batches = (
        ImportBatch.query
        .order_by(ImportBatch.uploaded_at.desc())
        .limit(50)
        .all()
    )
    return render_template(
        "import_pdf/history.html",
        batches=batches,
        issuer_labels=SUPPORTED_ISSUERS,
        active_page="import_pdf",
    )


# ── helpers ────────────────────────────────────────────────────────────────────

def _load_parse_errors(batch) -> list[dict]:
    if not batch.parse_errors_json:
        return []
    try:
        return json.loads(batch.parse_errors_json)
    except (json.JSONDecodeError, TypeError):
        return []


def _save_merchant_mapping(normalized: str, category_id: int) -> None:
    existing = MerchantMapping.query.filter_by(
        normalized=normalized, user_confirmed=True
    ).first()
    if existing:
        existing.category_id = category_id
    else:
        db.session.add(MerchantMapping(
            raw_pattern=normalized,
            normalized=normalized,
            user_confirmed=True,
            category_id=category_id,
        ))
    db.session.commit()
