from __future__ import annotations

import json
import os
import queue
import sqlite3
import threading
from datetime import datetime
from pathlib import Path

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

BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = Path(os.environ.get("DOCS_WORKER_DATA_DIR", str(BASE_DIR / "data"))).resolve()
STORAGE_DIR = DATA_DIR / "storage"
DB_PATH = DATA_DIR / "docs.db"

DATA_DIR.mkdir(parents=True, exist_ok=True)
STORAGE_DIR.mkdir(parents=True, exist_ok=True)

app = Flask(__name__)
job_queue: "queue.Queue[dict]" = queue.Queue()


def get_db() -> sqlite3.Connection:
    if "db" not in g:
        conn = sqlite3.connect(DB_PATH)
        conn.row_factory = sqlite3.Row
        g.db = conn
    return g.db


def init_db() -> None:
    db = sqlite3.connect(DB_PATH)
    db.executescript(
        """
        create table if not exists organizations (
            id integer primary key autoincrement,
            name text not null,
            org_code text not null unique,
            region text not null,
            tier text not null
        );
        create table if not exists teams (
            id integer primary key autoincrement,
            org_id integer not null,
            name text not null,
            cost_center text not null,
            status text not null default 'active'
        );
        create table if not exists users (
            id integer primary key autoincrement,
            org_id integer not null,
            team_id integer,
            email text not null,
            display_name text not null,
            role text not null,
            status text not null default 'active'
        );
        create table if not exists accounts (
            id integer primary key autoincrement,
            org_id integer not null,
            account_no text not null,
            currency text not null,
            balance numeric not null default 0
        );
        create table if not exists beneficiaries (
            id integer primary key autoincrement,
            org_id integer not null,
            name text not null,
            bank_name text not null,
            routing text not null
        );
        create table if not exists transfers (
            id integer primary key autoincrement,
            org_id integer not null,
            account_id integer not null,
            beneficiary_id integer not null,
            amount numeric not null,
            currency text not null,
            status text not null,
            memo text not null,
            requested_by text not null
        );
        create table if not exists documents (
            id integer primary key autoincrement,
            org_id integer not null,
            filename text not null,
            original_name text not null,
            mime_type text not null,
            kind text not null,
            created_at text not null default current_timestamp
        );
        create table if not exists risk_reviews (
            id integer primary key autoincrement,
            org_id integer not null,
            subject text not null,
            score numeric not null,
            note text not null,
            created_at text not null default current_timestamp
        );
        create table if not exists audit_records (
            id integer primary key autoincrement,
            org_id integer,
            actor text not null,
            action text not null,
            resource text not null,
            details text not null,
            created_at text not null default current_timestamp
        );
        """
    )
    if db.execute("select count(*) from organizations").fetchone()[0] == 0:
        db.execute(
            "insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)",
            ("Acme Treasury", "acme", "us-east", "enterprise"),
        )
        db.execute(
            "insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)",
            ("Northwind Holdings", "northwind", "eu-west", "growth"),
        )
        db.commit()
    db.close()


def current_identity() -> dict:
    return {
        "org_id": int(request.headers.get("X-Org-Id", "1")),
        "user_id": request.headers.get("X-User-Id", "docs-user"),
        "role": request.headers.get("X-Role", "viewer"),
    }


@app.before_request
def load_identity() -> None:
    g.identity = current_identity()
    g.started_at = datetime.utcnow().isoformat()


@app.after_request
def audit_request(response: Response) -> Response:
    identity = getattr(g, "identity", None)
    if identity:
        db = get_db()
        db.execute(
            "insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)",
            (
                identity["org_id"],
                identity["user_id"],
                f"{request.method} {request.path}",
                request.full_path,
                json.dumps({"status": response.status_code, "args": request.args.to_dict(), "json": request.get_json(silent=True)}),
            ),
        )
        db.commit()
    return response


@app.teardown_appcontext
def close_db(exc: Exception | None) -> None:
    db = g.pop("db", None)
    if db is not None:
        db.close()


def require_roles(*allowed: str):
    def decorator(fn):
        def wrapped(*args, **kwargs):
            if g.identity["role"] not in allowed:
                return jsonify({"error": "forbidden"}), 403
            return fn(*args, **kwargs)

        wrapped.__name__ = fn.__name__
        return wrapped

    return decorator


def render_report(org_name: str, title: str, notes: str) -> str:
    return f"""
    <html>
      <body>
        <h1>{title}</h1>
        <h2>{org_name}</h2>
        <div>{notes}</div>
      </body>
    </html>
    """


def resolve_storage_path(filename: str, *, must_exist: bool = False) -> Path:
    if not isinstance(filename, str) or not filename:
        raise ValueError("invalid filename")
    if filename in {".", ".."}:
        raise ValueError("invalid filename")
    if "\x00" in filename:
        raise ValueError("invalid filename")
    candidate_name = Path(filename).name
    if candidate_name != filename or Path(filename).is_absolute():
        raise ValueError("invalid filename")

    base_dir = STORAGE_DIR.resolve()
    target = base_dir / filename

    if must_exist:
        resolved = target.resolve(strict=True)
        if resolved.parent != base_dir:
            raise ValueError("invalid filename")
        return resolved

    if target.exists():
        resolved = target.resolve(strict=True)
        if resolved.parent != base_dir:
            raise ValueError("invalid filename")

    return target


def enqueue_job(payload: dict) -> None:
    job_queue.put(payload)


def job_worker() -> None:
    while True:
        payload = job_queue.get()
        if payload is None:
            break
        db = sqlite3.connect(DB_PATH)
        db.execute(
            "insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)",
            (
                payload.get("org_id"),
                "system",
                "job.dispatch",
                payload.get("kind", "document"),
                json.dumps(payload),
            ),
        )
        if payload.get("kind") == "document":
            filename = payload["filename"]
            target = STORAGE_DIR / filename
            target.write_text(payload.get("content", ""), encoding="utf-8")
            db.execute(
                "insert into documents (org_id, filename, original_name, mime_type, kind) values (?, ?, ?, ?, ?)",
                (
                    payload.get("org_id"),
                    filename,
                    payload.get("original_name", filename),
                    payload.get("mime_type", "text/plain"),
                    payload.get("kind", "document"),
                ),
            )
        db.commit()
        db.close()
        job_queue.task_done()


init_db()
threading.Thread(target=job_worker, daemon=True).start()


@app.route("/health", methods=["GET"])
def health() -> tuple:
    db = get_db()
    counts = {
        "documents": db.execute("select count(*) from documents").fetchone()[0],
        "audit_records": db.execute("select count(*) from audit_records").fetchone()[0],
        "jobs_pending": job_queue.qsize(),
    }
    return jsonify({"ok": True, "counts": counts})


@app.route("/documents", methods=["GET"])
@require_roles("admin", "manager", "viewer")
def search_documents() -> tuple:
    clauses = ["org_id = ?"]
    params: list[object] = [g.identity["org_id"]]
    kind = request.args.get("kind", "").strip()
    name = request.args.get("name", "").strip()
    if kind:
        clauses.append("kind = ?")
        params.append(kind)
    if name:
        clauses.append("original_name like ?")
        params.append(f"%{name}%")
    rows = get_db().execute(
        f"select id, filename, original_name, mime_type, kind, created_at from documents where {' and '.join(clauses)} order by id desc",
        params,
    ).fetchall()
    return jsonify({"data": [dict(row) for row in rows]})


@app.route("/docs/render", methods=["POST"])
@require_roles("admin", "manager")
def render_document() -> tuple:
    payload = request.get_json(force=True)
    org_name = payload["orgName"]
    title = payload.get("title", "Treasury Notice")
    notes = payload.get("notes", "")
    filename = payload.get("filename", f"report-{int(datetime.utcnow().timestamp())}.html")
    html = render_report(org_name, title, notes)
    try:
        target = resolve_storage_path(filename)
    except ValueError:
        return jsonify({"error": "invalid filename"}), 400
    target.write_text(html, encoding="utf-8")
    db = get_db()
    db.execute(
        "insert into documents (org_id, filename, original_name, mime_type, kind) values (?, ?, ?, ?, ?)",
        (g.identity["org_id"], filename, filename, "text/html", "report"),
    )
    db.commit()
    return jsonify({"filename": filename, "path": str(target)}), 201


@app.route("/docs/upload", methods=["POST"])
@require_roles("admin", "manager")
def upload_document() -> tuple:
    upload = request.files.get("file")
    if upload is None:
        return jsonify({"error": "missing file"}), 400
    filename = f"{int(datetime.utcnow().timestamp())}-{upload.filename}"
    try:
        target = resolve_storage_path(filename)
    except ValueError:
        return jsonify({"error": "invalid filename"}), 400
    upload.save(target)
    db = get_db()
    db.execute(
        "insert into documents (org_id, filename, original_name, mime_type, kind) values (?, ?, ?, ?, ?)",
        (g.identity["org_id"], filename, upload.filename, upload.mimetype or "application/octet-stream", "upload"),
    )
    db.commit()
    return jsonify({"filename": filename}), 201


@app.route("/docs/download/<path:filename>", methods=["GET"])
@require_roles("admin", "manager", "viewer")
def download_document(filename: str):
    try:
        target = resolve_storage_path(filename, must_exist=True)
    except FileNotFoundError:
        return jsonify({"error": "not found"}), 404
    except ValueError:
        return jsonify({"error": "invalid filename"}), 400
    return send_file(target, as_attachment=True)


@app.route("/jobs/dispatch", methods=["POST"])
@require_roles("admin", "manager")
def dispatch_job() -> tuple:
    payload = request.get_json(force=True)
    payload["org_id"] = g.identity["org_id"]
    enqueue_job(payload)
    return jsonify({"queued": True, "kind": payload.get("kind", "document")}), 202


@app.route("/reports/summary", methods=["GET"])
@require_roles("admin", "manager")
def summary_report() -> tuple:
    db = get_db()
    org_id = int(request.args.get("orgId", g.identity["org_id"]))
    title = request.args.get("title", "Document Summary")
    org = db.execute("select name, region, tier from organizations where id = ?", (org_id,)).fetchone()
    if org is None:
        return jsonify({"error": "missing org"}), 404
    notes = request.args.get("notes", "No notes supplied")
    html = render_report(org["name"], title, notes)
    return Response(html, mimetype="text/html")


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "4300")), debug=False)
