"""
IP Trademark Reporting Manager (IPTRM)
Self-hosted Flask app: upload -> extract -> edit -> report -> email.
"""
import os
import io
import functools
import datetime as dt
from flask import (
    Flask, render_template, request, redirect, url_for, flash,
    send_file, abort, jsonify, Response, session, g,
)
from werkzeug.utils import secure_filename

from .config import config
from .models import (
    SessionLocal, init_db, Issue, Application, Subscriber, Invoice, EmailLog,
    Transliteration, User, FieldTranslation, WatchedMark, ConflictAlert,
)
from .services import extractor, reporter, mailer, translator, transliterator
from .services import search as search_service
from .services import conflicts as conflicts_service

app = Flask(__name__)
app.config["SECRET_KEY"] = config.SECRET_KEY
app.config["MAX_CONTENT_LENGTH"] = config.MAX_CONTENT_LENGTH
app.config["PERMANENT_SESSION_LIFETIME"] = dt.timedelta(hours=12)

os.makedirs(config.UPLOAD_DIR, exist_ok=True)
os.makedirs(os.path.join(config.BASE_DIR if hasattr(config, "BASE_DIR") else ".", "instance"), exist_ok=True)


def db():
    return SessionLocal()


# Endpoints reachable without login.
_PUBLIC_ENDPOINTS = {"login", "healthz", "static"}


@app.before_request
def require_login():
    """Protect every route except the public ones. Loads g.user for the request."""
    g.user = None
    uid = session.get("uid")
    if uid:
        s = db()
        u = s.get(User, uid)
        if u and u.active:
            g.user = {"id": u.id, "username": u.username, "role": u.role,
                      "is_admin": u.is_admin}
        s.close()
    if request.endpoint in _PUBLIC_ENDPOINTS:
        return
    if g.user is None:
        if request.method == "GET":
            return redirect(url_for("login", next=request.path))
        abort(401)


@app.context_processor
def inject_user():
    open_alerts = 0
    # Only query when a user is logged in (skips the login page / unauthenticated hits).
    if getattr(g, "user", None):
        s = db()
        try:
            open_alerts = s.query(ConflictAlert).filter_by(status="open").count()
        except Exception:
            open_alerts = 0
        finally:
            s.close()
    return {"current_user": getattr(g, "user", None),
            "open_alert_count": open_alerts}


def admin_required(fn):
    """Block viewers from state-changing admin actions."""
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        if not g.user or not g.user.get("is_admin"):
            abort(403)
        return fn(*args, **kwargs)
    return wrapper


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = (request.form.get("username") or "").strip()
        password = request.form.get("password") or ""
        s = db()
        u = s.query(User).filter_by(username=username).first()
        if u and u.active and u.check_password(password):
            u.last_login = dt.datetime.utcnow()
            s.commit()
            session.permanent = True
            session["uid"] = u.id
            s.close()
            nxt = request.args.get("next") or url_for("dashboard")
            return redirect(nxt)
        s.close()
        flash("Invalid username or password.", "warn")
    return render_template("login.html")


@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))


def _detect_applicant_type(applicant, entity):
    """Heuristic: company if it has an entity type or a company keyword;
    individual if it starts with a personal honorific."""
    a = (applicant or "")
    if entity:
        return "company"
    company_kw = ["شرکت", "شركت", "موسسه", "مؤسسه", "تعاونی", "بازرگانی", "صنایع", "گروه"]
    if any(k in a for k in company_kw):
        return "company"
    individual_kw = ["آقای", "آقاي", "خانم", "اقای"]
    if any(a.strip().startswith(k) for k in individual_kw):
        return "individual"
    return None


# ---------------------------------------------------------------- Dashboard
@app.route("/")
def dashboard():
    s = db()
    issues = s.query(Issue).order_by(Issue.publication_date.desc()).limit(10).all()
    stats = {
        "issues": s.query(Issue).count(),
        "applications": s.query(Application).count(),
        "subscribers": s.query(Subscriber).filter_by(active=True).count(),
        "open_issues": s.query(Issue).filter_by(status="open").count(),
    }
    # opposition deadlines coming up in next 7 days
    today = dt.date.today()
    upcoming = []
    for iss in s.query(Issue).all():
        dl = iss.opposition_deadline
        if dl and today <= dl <= today + dt.timedelta(days=7):
            upcoming.append(iss)
    out = render_template("dashboard.html", issues=issues, stats=stats,
                          upcoming=upcoming, today=today)
    s.close()
    return out


# ---------------------------------------------------------------- Search archive
@app.route("/search")
def search_archive():
    s = db()
    q = request.args.get("q") or None
    nice_class = request.args.get("nice_class") or None
    applicant = request.args.get("applicant") or None
    date_from = request.args.get("date_from") or None
    date_to = request.args.get("date_to") or None
    status = request.args.get("status") or None
    has_query = any([q, nice_class, applicant, date_from, date_to, status])
    results = []
    if has_query:
        results = search_service.search(
            s, q=q, nice_class=nice_class, applicant=applicant,
            date_from=date_from, date_to=date_to, status=status)
    out = render_template("search.html", results=results, has_query=has_query,
                          q=q or "", nice_class=nice_class or "",
                          applicant=applicant or "", date_from=date_from or "",
                          date_to=date_to or "", status=status or "")
    s.close()
    return out


@app.route("/applications/<int:app_id>")
def application_detail(app_id):
    s = db()
    from sqlalchemy.orm import joinedload
    a = (s.query(Application)
         .options(joinedload(Application.transliterations),
                  joinedload(Application.issue))
         .filter(Application.id == app_id).first())
    if not a:
        s.close(); abort(404)
    out = render_template("application_detail.html", a=a)
    s.close()
    return out


# ---------------------------------------------------------------- Issues
@app.route("/issues")
def issues():
    s = db()
    items = s.query(Issue).order_by(Issue.publication_date.desc()).all()
    out = render_template("issues.html", issues=items)
    s.close()
    return out


@app.route("/issues/new", methods=["POST"])
@admin_required
def new_issue():
    s = db()
    pub = request.form.get("publication_date")
    expected = int(request.form.get("expected_count") or 0)
    iss = Issue(
        publication_date=dt.date.fromisoformat(pub) if pub else None,
        expected_count=expected,
        title=request.form.get("title") or "Iran TM Bulletin",
    )
    s.add(iss)
    s.commit()
    iid = iss.id
    s.close()
    flash("Issue created. Now upload the application PDFs.", "ok")
    return redirect(url_for("issue_detail", issue_id=iid))


@app.route("/issues/<int:issue_id>")
def issue_detail(issue_id):
    s = db()
    iss = s.get(Issue, issue_id)
    if not iss:
        s.close(); abort(404)
    apps = iss.applications
    # Per-subscriber preview: how many applications each active subscriber will get.
    subs = s.query(Subscriber).filter_by(active=True).all()
    sub_preview = []
    for sub in subs:
        watched = sub.watched_classes()
        n = sum(1 for a in apps if a.matches_classes(watched))
        sub_preview.append({"sub": sub, "matched": n,
                            "classes": sub.classes_of_interest or "all"})
    data = render_template("issue_detail.html", issue=iss, applications=apps,
                           designs=reporter.DESIGNS, languages=reporter.LANGUAGES,
                           sub_preview=sub_preview)
    s.close()
    return data


@app.route("/issues/<int:issue_id>/upload", methods=["POST"])
@admin_required
def upload(issue_id):
    s = db()
    iss = s.get(Issue, issue_id)
    if not iss:
        s.close(); abort(404)
    files = request.files.getlist("pdfs")
    issue_dir = os.path.join(config.UPLOAD_DIR, f"issue_{issue_id}")
    os.makedirs(issue_dir, exist_ok=True)
    added, flagged = 0, 0
    for i, f in enumerate(files):
        if not f.filename.lower().endswith(".pdf"):
            continue
        fname = secure_filename(f.filename)
        path = os.path.join(issue_dir, fname)
        f.save(path)
        data, ok = extractor.extract_application(path, issue_dir, app_idx=i)
        tr = translator.translate_application(data, target="en")
        # Build transliteration variants (PDF Latin > API > rule-based).
        variants = transliterator.build_variants(
            data["mark_text"],
            latin_from_pdf=data["mark_text_en"],
            api_translit=tr.get("mark_translit"),
        )
        primary_translit = variants[0]["value"] if variants else (
            tr.get("mark_translit") or data["mark_text_en"])
        # Applicant type: company markers vs individual (آقای/خانم).
        applicant_type = _detect_applicant_type(data["owner"], data["owner_entity"])
        a = Application(
            issue_id=issue_id,
            pub_number=data["adv_number"],
            pub_date=data["adv_date"],
            pub_date_raw=data["adv_date_raw"],
            app_number=data["app_number"],
            app_date=data["app_date"],
            app_date_raw=data["app_date_raw"],
            mark_text=data["mark_text"],
            mark_translit=primary_translit,
            mark_translation=tr.get("mark_translation"),
            applicant=data["owner"],
            applicant_en=tr.get("applicant_en"),
            applicant_type=applicant_type,
            applicant_entity=data["owner_entity"],
            applicant_entity_en=tr.get("applicant_entity_en"),
            applicant_reg_no=data["owner_reg_no"],
            nationality=data["nationality"],
            nationality_en=tr.get("nationality_en"),
            legal_rep=data["legal_rep"],
            address=data["address"],
            nice_class=data["nice_class"],
            goods_services=data["goods_services"],
            goods_services_en=tr.get("goods_services_en"),
            disclaimer=data["disclaimer"],
            disclaimer_en=tr.get("disclaimer_en"),
            mark_image_path=data["mark_image_path"],
            source_pdf_path=path,
            source_pdf_name=fname,
            extraction_ok=ok,
            verified=False,
        )
        for v in variants:
            a.transliterations.append(Transliteration(
                value=v["value"], kind=v["type"], is_primary=v["is_primary"]))
        s.add(a)
        added += 1
        if not ok:
            flagged += 1
        # Auto-fill the issue's publication date from the first PDF's publication date.
        if data["adv_date"] and not iss.publication_date:
            iss.publication_date = data["adv_date"]
    s.commit()
    # Auto-scan the newly uploaded applications against the watchlist.
    alerts_n = _scan_issue_conflicts(s, iss)
    s.close()
    msg = (f"Uploaded {added} file(s). {flagged} flagged for review "
           f"(low extraction confidence).")
    if alerts_n:
        msg += f" {alerts_n} conflict alert(s) raised — see Alerts."
    flash(msg, "ok" if not (flagged or alerts_n) else "warn")
    return redirect(url_for("issue_detail", issue_id=issue_id))


# ---------------------------------------------------------------- Edit application
@app.route("/applications/<int:app_id>/edit", methods=["GET", "POST"])
def edit_application(app_id):
    s = db()
    a = s.get(Application, app_id)
    if not a:
        s.close(); abort(404)
    if request.method == "POST":
        if not g.user or not g.user.get("is_admin"):
            abort(403)
        for field in ["pub_number", "app_number", "mark_text", "mark_translit",
                      "mark_translation", "applicant", "applicant_en",
                      "applicant_type", "applicant_entity", "applicant_entity_en",
                      "applicant_reg_no", "nationality", "nationality_en",
                      "legal_rep", "address", "nice_class", "goods_services",
                      "goods_services_en", "disclaimer", "disclaimer_en"]:
            setattr(a, field, request.form.get(field) or None)
        adate = request.form.get("app_date")
        a.app_date = dt.date.fromisoformat(adate) if adate else None
        pdate = request.form.get("pub_date")
        a.pub_date = dt.date.fromisoformat(pdate) if pdate else None
        a.verified = bool(request.form.get("verified"))
        a.extraction_ok = True
        s.commit()
        iid = a.issue_id
        s.close()
        flash("Application updated.", "ok")
        return redirect(url_for("issue_detail", issue_id=iid))
    data = render_template("edit_application.html", a=a)
    s.close()
    return data


@app.route("/applications/<int:app_id>/translit/add", methods=["POST"])
@admin_required
def add_translit(app_id):
    s = db()
    a = s.get(Application, app_id)
    if not a:
        s.close(); abort(404)
    value = (request.form.get("value") or "").strip()
    if value and not any(t.value.lower() == value.lower() for t in a.transliterations):
        a.transliterations.append(Transliteration(
            value=value, kind=transliterator.MANUAL,
            is_primary=not a.transliterations))  # primary if it's the first
        s.commit()
    s.close()
    return redirect(url_for("edit_application", app_id=app_id))


@app.route("/applications/<int:app_id>/translit/<int:tid>/delete", methods=["POST"])
@admin_required
def delete_translit(app_id, tid):
    s = db()
    t = s.get(Transliteration, tid)
    if t and t.application_id == app_id:
        was_primary = t.is_primary
        s.delete(t); s.flush()
        if was_primary:
            a = s.get(Application, app_id)
            if a.transliterations:
                a.transliterations[0].is_primary = True
        s.commit()
    s.close()
    return redirect(url_for("edit_application", app_id=app_id))


@app.route("/applications/<int:app_id>/translit/<int:tid>/primary", methods=["POST"])
@admin_required
def set_primary_translit(app_id, tid):
    s = db()
    a = s.get(Application, app_id)
    if a:
        for t in a.transliterations:
            t.is_primary = (t.id == tid)
            if t.id == tid:
                a.mark_translit = t.value
        s.commit()
    s.close()
    return redirect(url_for("edit_application", app_id=app_id))


@app.route("/applications/<int:app_id>/translate", methods=["POST"])
@admin_required
def translate_application_route(app_id):
    s = db()
    a = s.get(Application, app_id)
    if not a:
        s.close(); abort(404)
    if not translator.is_configured():
        s.close()
        flash("Translation is not configured. Set ANTHROPIC_API_KEY to enable "
              "auto-translation, or fill the target-language fields manually.", "warn")
        return redirect(url_for("edit_application", app_id=app_id))
    src = {"mark_text": a.mark_text, "owner": a.applicant,
           "owner_entity": a.applicant_entity, "nationality": a.nationality,
           "goods_services": a.goods_services, "disclaimer": a.disclaimer,
           "mark_text_en": a.mark_translit}
    tr = translator.translate_application(src, target="en")
    if tr.get("mark_translit"):
        a.mark_translit = tr["mark_translit"]
    a.mark_translation = tr.get("mark_translation") or a.mark_translation
    a.applicant_en = tr.get("applicant_en") or a.applicant_en
    a.applicant_entity_en = tr.get("applicant_entity_en") or a.applicant_entity_en
    a.nationality_en = tr.get("nationality_en") or a.nationality_en
    a.goods_services_en = tr.get("goods_services_en") or a.goods_services_en
    a.disclaimer_en = tr.get("disclaimer_en") or a.disclaimer_en
    s.commit()
    s.close()
    flash("Re-translated target-language fields.", "ok")
    return redirect(url_for("edit_application", app_id=app_id))


@app.route("/applications/<int:app_id>/translate/<lang>", methods=["POST"])
@admin_required
def translate_to_language(app_id, lang):
    """Translate an application into a non-English target language (zh, ar, …)
    and store the result in field_translations."""
    if lang in ("en", "fa"):
        abort(400)
    s = db()
    a = s.get(Application, app_id)
    if not a:
        s.close(); abort(404)
    if not translator.is_configured():
        s.close()
        flash("Translation is not configured (set ANTHROPIC_API_KEY).", "warn")
        return redirect(url_for("edit_application", app_id=app_id))
    src = {"mark_text": a.mark_text, "owner": a.applicant,
           "owner_entity": a.applicant_entity, "nationality": a.nationality,
           "goods_services": a.goods_services, "disclaimer": a.disclaimer,
           "mark_text_en": a.mark_translit}
    tr = translator.translate_application(src, target=lang)
    mapping = {
        "mark_translit": tr.get("mark_translit"),
        "mark_translation": tr.get("mark_translation"),
        "applicant": tr.get("applicant_en"),
        "goods_services": tr.get("goods_services_en"),
        "disclaimer": tr.get("disclaimer_en"),
    }
    # upsert each field translation
    existing = {(ft.field): ft for ft in a.field_translations if ft.lang == lang}
    for field, val in mapping.items():
        if not val:
            continue
        if field in existing:
            existing[field].value = val
        else:
            a.field_translations.append(
                FieldTranslation(lang=lang, field=field, value=val))
    s.commit()
    s.close()
    flash(f"Translated into {reporter.LANGUAGES.get(lang, lang)}.", "ok")
    return redirect(url_for("edit_application", app_id=app_id))


@app.route("/applications/<int:app_id>/delete", methods=["POST"])
@admin_required
def delete_application(app_id):
    s = db()
    a = s.get(Application, app_id)
    if a:
        iid = a.issue_id
        s.delete(a); s.commit()
        s.close()
        return redirect(url_for("issue_detail", issue_id=iid))
    s.close(); abort(404)


@app.route("/source/<int:app_id>")
def source_file(app_id):
    s = db()
    a = s.get(Application, app_id)
    s.close()
    if not a or not a.source_pdf_path or not os.path.exists(a.source_pdf_path):
        abort(404)
    return send_file(a.source_pdf_path, as_attachment=True,
                     download_name=a.source_pdf_name)


@app.route("/mark-image/<int:app_id>")
def mark_image(app_id):
    s = db()
    a = s.get(Application, app_id)
    s.close()
    if not a or not a.mark_image_path or not os.path.exists(a.mark_image_path):
        abort(404)
    return send_file(a.mark_image_path)


# ---------------------------------------------------------------- Reports
@app.route("/issues/<int:issue_id>/report")
def report(issue_id):
    s = db()
    iss = s.get(Issue, issue_id)
    if not iss:
        s.close(); abort(404)
    fmt = request.args.get("format", "pdf")
    design = request.args.get("design", "bulletin")
    lang = request.args.get("lang", "en")
    apps = iss.applications
    base_url = request.url_root.rstrip("/")
    out = reporter.generate(iss, apps, fmt=fmt, design=design, lang=lang,
                            base_url=base_url)
    pubdate = iss.publication_date
    s.close()
    if fmt == "html":
        return Response(out, mimetype="text/html")
    return send_file(io.BytesIO(out), mimetype=reporter.CONTENT_TYPES[fmt],
                     as_attachment=(fmt != "html"),
                     download_name=f"Iran_TM_{pubdate}.{fmt}")


@app.route("/issues/<int:issue_id>/mark-complete", methods=["POST"])
@admin_required
def mark_complete(issue_id):
    s = db()
    iss = s.get(Issue, issue_id)
    if iss:
        iss.status = "complete"; s.commit()
        flash("Issue marked complete — ready to send.", "ok")
    s.close()
    return redirect(url_for("issue_detail", issue_id=issue_id))


@app.route("/issues/<int:issue_id>/preview/<int:sub_id>")
def preview_for_subscriber(issue_id, sub_id):
    """Preview exactly what a given subscriber will receive (class-filtered)."""
    s = db()
    iss = s.get(Issue, issue_id)
    sub = s.get(Subscriber, sub_id)
    if not iss or not sub:
        s.close(); abort(404)
    watched = sub.watched_classes()
    matched = [a for a in iss.applications if a.matches_classes(watched)]
    fmt = request.args.get("format", sub.pref_format or "pdf")
    base_url = request.url_root.rstrip("/")
    out = reporter.generate(iss, matched, fmt=fmt, design=sub.pref_design or "bulletin",
                            lang=sub.pref_language or "en", base_url=base_url)
    pubdate = iss.publication_date
    s.close()
    if fmt == "html":
        return Response(out, mimetype="text/html")
    return send_file(io.BytesIO(out), mimetype=reporter.CONTENT_TYPES[fmt],
                     as_attachment=True, download_name=f"Iran_TM_{pubdate}_{sub_id}.{fmt}")


@app.route("/logs")
def email_logs():
    s = db()
    from sqlalchemy.orm import joinedload
    logs = (s.query(EmailLog).order_by(EmailLog.sent_at.desc()).limit(200).all())
    # resolve subscriber names for display
    sub_map = {x.id: x.name for x in s.query(Subscriber).all()}
    out = render_template("logs.html", logs=logs, sub_map=sub_map)
    s.close()
    return out


@app.route("/issues/<int:issue_id>/send", methods=["POST"])
@admin_required
def send_issue(issue_id):
    s = db()
    iss = s.get(Issue, issue_id)
    if not iss:
        s.close(); abort(404)
    subs = s.query(Subscriber).filter_by(active=True).all()
    base_url = request.url_root.rstrip("/")
    results = mailer.send_issue_to_all(s, iss, iss.applications, subs, base_url)
    iss.status = "sent"
    iss.sent_at = dt.datetime.utcnow()
    s.commit()
    s.close()
    msg = (f"Sent to {results['sent']} subscriber(s). "
           f"{results['failed']} failed, {results['skipped']} skipped "
           f"(no matching classes).")
    flash(msg, "ok" if not results["failed"] else "warn")
    return redirect(url_for("issue_detail", issue_id=issue_id))


# ---------------------------------------------------------------- Subscribers
@app.route("/subscribers")
def subscribers():
    s = db()
    items = s.query(Subscriber).order_by(Subscriber.name).all()
    s.close()
    return render_template("subscribers.html", subscribers=items,
                           designs=reporter.DESIGNS, languages=reporter.LANGUAGES)


@app.route("/subscribers/new", methods=["POST"])
@admin_required
def new_subscriber():
    s = db()
    sub = Subscriber(
        name=request.form["name"], email=request.form["email"],
        company=request.form.get("company"),
        pref_format=request.form.get("pref_format", "pdf"),
        pref_design=request.form.get("pref_design", "bulletin"),
        pref_language=request.form.get("pref_language", "en"),
        classes_of_interest=(request.form.get("classes_of_interest") or "").strip() or None,
        monthly_fee=float(request.form.get("monthly_fee") or 0),
    )
    s.add(sub); s.commit(); s.close()
    flash("Subscriber added.", "ok")
    return redirect(url_for("subscribers"))


@app.route("/subscribers/<int:sub_id>/edit", methods=["POST"])
@admin_required
def edit_subscriber(sub_id):
    s = db()
    sub = s.get(Subscriber, sub_id)
    if not sub:
        s.close(); abort(404)
    sub.name = request.form.get("name") or sub.name
    sub.email = request.form.get("email") or sub.email
    sub.company = request.form.get("company") or None
    sub.pref_format = request.form.get("pref_format", sub.pref_format)
    sub.pref_design = request.form.get("pref_design", sub.pref_design)
    sub.pref_language = request.form.get("pref_language", sub.pref_language)
    sub.classes_of_interest = (request.form.get("classes_of_interest") or "").strip() or None
    fee = request.form.get("monthly_fee")
    if fee is not None and fee != "":
        sub.monthly_fee = float(fee)
    s.commit(); s.close()
    flash("Subscriber updated.", "ok")
    return redirect(url_for("subscribers"))


@app.route("/subscribers/<int:sub_id>/toggle", methods=["POST"])
@admin_required
def toggle_subscriber(sub_id):
    s = db()
    sub = s.get(Subscriber, sub_id)
    if sub:
        sub.active = not sub.active; s.commit()
    s.close()
    return redirect(url_for("subscribers"))


# ---------------------------------------------------------------- Invoices
@app.route("/invoices")
def invoices():
    from sqlalchemy.orm import joinedload
    s = db()
    items = (s.query(Invoice).options(joinedload(Invoice.subscriber))
             .order_by(Invoice.issued_at.desc()).all())
    subs = s.query(Subscriber).filter_by(active=True).all()
    # render while session is open so lazy relationships resolve
    out = render_template("invoices.html", invoices=items, subscribers=subs)
    s.close()
    return out


@app.route("/invoices/generate", methods=["POST"])
@admin_required
def generate_invoices():
    """Generate invoices for the chosen period for all active subscribers."""
    s = db()
    start = dt.date.fromisoformat(request.form["period_start"])
    end = dt.date.fromisoformat(request.form["period_end"])
    subs = s.query(Subscriber).filter_by(active=True).all()
    count = 0
    for sub in subs:
        if not sub.monthly_fee:
            continue
        num = f"INV-{end.strftime('%Y%m')}-{sub.id:04d}"
        if s.query(Invoice).filter_by(number=num).first():
            continue
        inv = Invoice(subscriber_id=sub.id, number=num, period_start=start,
                      period_end=end, amount=sub.monthly_fee)
        s.add(inv); count += 1
    s.commit(); s.close()
    flash(f"Generated {count} invoice(s).", "ok")
    return redirect(url_for("invoices"))


@app.route("/invoices/<int:inv_id>/paid", methods=["POST"])
@admin_required
def mark_paid(inv_id):
    s = db()
    inv = s.get(Invoice, inv_id)
    if inv:
        inv.paid = not inv.paid; s.commit()
    s.close()
    return redirect(url_for("invoices"))


@app.route("/users")
@admin_required
def users():
    s = db()
    items = s.query(User).order_by(User.username).all()
    out = render_template("users.html", users=items)
    s.close()
    return out


@app.route("/users/new", methods=["POST"])
@admin_required
def new_user():
    s = db()
    username = (request.form.get("username") or "").strip()
    password = request.form.get("password") or ""
    role = request.form.get("role", "viewer")
    if not username or not password:
        s.close()
        flash("Username and password are required.", "warn")
        return redirect(url_for("users"))
    if s.query(User).filter_by(username=username).first():
        s.close()
        flash("That username already exists.", "warn")
        return redirect(url_for("users"))
    u = User(username=username, role=("admin" if role == "admin" else "viewer"))
    u.set_password(password)
    s.add(u); s.commit(); s.close()
    flash("User created.", "ok")
    return redirect(url_for("users"))


@app.route("/users/<int:user_id>/toggle", methods=["POST"])
@admin_required
def toggle_user(user_id):
    s = db()
    u = s.get(User, user_id)
    if u:
        # don't let an admin deactivate themselves
        if u.id == g.user["id"]:
            flash("You can't deactivate your own account.", "warn")
        else:
            u.active = not u.active
            s.commit()
    s.close()
    return redirect(url_for("users"))


@app.route("/account", methods=["GET", "POST"])
def account():
    s = db()
    u = s.get(User, g.user["id"])
    if request.method == "POST":
        cur = request.form.get("current") or ""
        new = request.form.get("new") or ""
        if not u.check_password(cur):
            flash("Current password is incorrect.", "warn")
        elif len(new) < 6:
            flash("New password must be at least 6 characters.", "warn")
        else:
            u.set_password(new); s.commit()
            flash("Password updated.", "ok")
    out = render_template("account.html", u=u)
    s.close()
    return out


# ---------------------------------------------------------------- Watchlist & conflicts
def _scan_issue_conflicts(session, issue, threshold=60):
    """Score every application in an issue against active watched marks,
    creating ConflictAlert rows. Returns the number of new alerts."""
    watched = session.query(WatchedMark).filter_by(active=True).all()
    created = 0
    for a in issue.applications:
        for w in watched:
            # skip if an alert already exists for this pair
            exists = (session.query(ConflictAlert)
                      .filter_by(watched_id=w.id, application_id=a.id).first())
            if exists:
                continue
            res = conflicts_service.evaluate(w, a)
            if res and res["score"] >= threshold:
                session.add(ConflictAlert(
                    watched_id=w.id, application_id=a.id,
                    score=res["score"], kind=res["kind"],
                    matched_on=res["matched_on"],
                    class_overlap=res["class_overlap"]))
                created += 1
    session.commit()
    return created


@app.route("/watchlist")
def watchlist():
    s = db()
    items = s.query(WatchedMark).order_by(WatchedMark.created_at.desc()).all()
    out = render_template("watchlist.html", marks=items)
    s.close()
    return out


@app.route("/watchlist/new", methods=["POST"])
@admin_required
def new_watched_mark():
    s = db()
    label = (request.form.get("label") or "").strip()
    if not label:
        s.close()
        flash("A mark label is required.", "warn")
        return redirect(url_for("watchlist"))
    w = WatchedMark(
        label=label,
        farsi=(request.form.get("farsi") or "").strip() or None,
        client_name=(request.form.get("client_name") or "").strip() or None,
        classes=(request.form.get("classes") or "").strip() or None,
    )
    s.add(w); s.commit()
    wid = w.id
    s.close()
    flash("Watched mark added. Run a scan to check existing applications.", "ok")
    return redirect(url_for("watchlist"))


@app.route("/watchlist/<int:wid>/toggle", methods=["POST"])
@admin_required
def toggle_watched_mark(wid):
    s = db()
    w = s.get(WatchedMark, wid)
    if w:
        w.active = not w.active; s.commit()
    s.close()
    return redirect(url_for("watchlist"))


@app.route("/watchlist/<int:wid>/delete", methods=["POST"])
@admin_required
def delete_watched_mark(wid):
    s = db()
    w = s.get(WatchedMark, wid)
    if w:
        s.delete(w); s.commit()
    s.close()
    return redirect(url_for("watchlist"))


@app.route("/issues/<int:issue_id>/scan", methods=["POST"])
@admin_required
def scan_issue(issue_id):
    s = db()
    iss = s.get(Issue, issue_id)
    if not iss:
        s.close(); abort(404)
    n = _scan_issue_conflicts(s, iss)
    s.close()
    flash(f"Scan complete — {n} new conflict alert(s).", "ok" if n == 0 else "warn")
    return redirect(url_for("alerts"))


@app.route("/alerts")
def alerts():
    s = db()
    from sqlalchemy.orm import joinedload
    status = request.args.get("status", "open")
    q = (s.query(ConflictAlert)
         .options(joinedload(ConflictAlert.watched),
                  joinedload(ConflictAlert.application))
         .order_by(ConflictAlert.class_overlap.desc(),
                   ConflictAlert.score.desc(), ConflictAlert.id.desc()))
    if status in ("open", "dismissed", "actioned"):
        q = q.filter(ConflictAlert.status == status)
    items = q.limit(300).all()
    open_count = s.query(ConflictAlert).filter_by(status="open").count()
    out = render_template("alerts.html", alerts=items, status=status,
                          open_count=open_count)
    s.close()
    return out


@app.route("/alerts/<int:aid>/<action>", methods=["POST"])
@admin_required
def alert_action(aid, action):
    if action not in ("dismiss", "action", "reopen"):
        abort(400)
    s = db()
    al = s.get(ConflictAlert, aid)
    if al:
        al.status = {"dismiss": "dismissed", "action": "actioned",
                     "reopen": "open"}[action]
        s.commit()
    s.close()
    return redirect(url_for("alerts"))


@app.route("/healthz")
def healthz():
    return jsonify(status="ok")


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)
