import os
import threading
import time

from flask import Flask, g, jsonify, request, send_file

from db import get_db, init_db
from middleware import load_session, log_audit, require_role
from pathsafe import resolve_within
from search_query import build_search_query

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STORAGE_ROOT = os.path.join(BASE_DIR, "storage")

app = Flask(__name__)
app.before_request(load_session)
app.after_request(log_audit)


@app.route("/healthz", methods=["GET"])
def healthz():
    return jsonify({"status": "ok", "service": "docs-worker"})


@app.route("/metrics", methods=["GET"])
def metrics():
    db = get_db()
    docs = db.execute("SELECT COUNT(*) AS c FROM documents").fetchone()["c"]
    processed = db.execute(
        "SELECT COUNT(*) AS c FROM documents WHERE status = 'processed'"
    ).fetchone()["c"]
    return jsonify({"documents_total": docs, "documents_processed": processed})


@app.route("/api/documents/search", methods=["GET"])
def search_documents():
    """Lets ops search the document library by type, status and a free
    text filename match, combining whatever filters were passed."""
    org_id = g.org_id
    doc_type = request.args.get("doc_type")
    status = request.args.get("status")
    q = request.args.get("q")

    # Build the filter with placeholders and bind every untrusted value; only
    # SQL structure lives in the query string (see search_query.build_search_query).
    query, params = build_search_query(org_id, doc_type, status, q)

    db = get_db()
    rows = db.execute(query, params).fetchall()
    documents = [dict(row) for row in rows]
    return jsonify({"documents": documents, "query": query})


@app.route("/api/documents/<org_id>/upload", methods=["POST"])
@require_role("admin", "analyst")
def upload_document(org_id):
    uploaded = request.files.get("document")
    if uploaded is None:
        return jsonify({"error": "document file is required"}), 400

    doc_type = request.form.get("docType", "other")
    dest_name = request.form.get("filename") or uploaded.filename

    org_dir = os.path.join(STORAGE_ROOT, f"org_{org_id}")
    os.makedirs(org_dir, exist_ok=True)

    dest_path = os.path.join(org_dir, dest_name)
    uploaded.save(dest_path)

    db = get_db()
    cur = db.execute(
        "INSERT INTO documents (org_id, filename, doc_type, status, uploaded_by) VALUES (?, ?, ?, 'uploaded', ?)",
        (org_id, dest_name, doc_type, g.user_email),
    )
    db.commit()

    return jsonify({"id": cur.lastrowid, "filename": dest_name, "orgId": org_id}), 201


@app.route("/api/documents/<org_id>/download", methods=["GET"])
def download_document(org_id):
    file_name = request.args.get("file")
    if not file_name:
        return jsonify({"error": "file query parameter is required"}), 400

    org_dir = os.path.join(STORAGE_ROOT, f"org_{org_id}")

    # Canonicalize the requested path and confirm it stays inside the org's
    # storage directory before touching the filesystem. This blocks "..",
    # absolute paths, null bytes and symlinked escapes (uploads share this
    # directory, so an attacker could otherwise plant a symlink to escape it).
    target = resolve_within(org_dir, file_name)
    if target is None:
        return jsonify({"error": "invalid file path"}), 400

    if not os.path.isfile(target):
        return jsonify({"error": "document not found"}), 404

    return send_file(target, as_attachment=True, download_name=os.path.basename(target))


REPORT_TEMPLATE = """
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>{{ title }}</title></head>
<body>
  <h1>{{ title }}</h1>
  <p>Prepared for: {{ org_name }}</p>
  <div class="analyst-note">{{ note }}</div>
</body>
</html>
"""

# Compile the report template once from a trusted constant. The caller only
# ever supplies context values (title/org name/note) -- never the template
# source -- so it cannot inject template syntax (SSTI). Flask's Jinja
# environment autoescapes string templates, so the values are HTML-escaped in
# the output as well (XSS).
_REPORT_TEMPLATE = app.jinja_env.from_string(REPORT_TEMPLATE)


@app.route("/api/reports/render", methods=["POST"])
def render_report():
    """Renders an ad-hoc analyst report. The title/org name/note fields are
    supplied by the caller so analysts can annotate the report however they
    like before it goes out to the customer."""
    payload = request.get_json(force=True, silent=True) or {}
    title = payload.get("title", "Document Review Report")
    org_name = payload.get("orgName", "")
    note = payload.get("note", "")

    html = _REPORT_TEMPLATE.render(title=title, org_name=org_name, note=note)
    return html, 200, {"Content-Type": "text/html"}


def _process_document_job(document_id, org_id):
    time.sleep(1)
    db = get_db()
    db.execute("UPDATE documents SET status = 'processed' WHERE id = ?", (document_id,))
    db.execute(
        "INSERT INTO audit_log (org_id, actor, action, detail) VALUES (?, ?, ?, ?)",
        (org_id, "docs-worker", "document.processed", f"document_id={document_id}"),
    )
    db.commit()


@app.route("/api/jobs/process", methods=["POST"])
@require_role("admin", "analyst")
def dispatch_processing_job():
    payload = request.get_json(force=True, silent=True) or {}
    document_id = payload.get("documentId")
    if not document_id:
        return jsonify({"error": "documentId is required"}), 400

    thread = threading.Thread(target=_process_document_job, args=(document_id, g.org_id), daemon=True)
    thread.start()

    return jsonify({"documentId": document_id, "status": "processing"}), 202


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 4002)))
