"""維港帳房：本機單機投資組合、股票及 MPF 持倉帳本。

執行：python3 app.py
開啟：http://127.0.0.1:8787
"""
from __future__ import annotations

import hashlib
import json
import os
import sqlite3
import time
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from decimal import Decimal, ROUND_CEILING, ROUND_HALF_UP
from functools import wraps
from pathlib import Path

from flask import Flask, jsonify, request, send_from_directory, session
from werkzeug.security import check_password_hash, generate_password_hash

BASE_DIR = Path(__file__).resolve().parent
# ponytail: allow LEDGER_DB_PATH env override so prod can pin the DB under /opt/data/diary/
DB_PATH = Path(os.getenv("LEDGER_DB_PATH") or str(BASE_DIR / "ledger.db"))
STATIC_DIR = BASE_DIR / "static"
ALLOWED_TRANSACTION_TYPES = {
    "BUY", "SELL", "DIVIDEND", "SPLIT", "MERGE", "MPF_CONTRIBUTION", "MPF_WITHDRAWAL", "ADJUSTMENT"
}

app = Flask(__name__, static_folder=str(STATIC_DIR), static_url_path="/static")
# 本機工具的簽名密鑰可用環境變數覆蓋；請不要把 ledger.db 上載至公開地方。
app.secret_key = os.getenv("LEDGER_SECRET", hashlib.sha256(str(BASE_DIR).encode()).hexdigest())
app.config.update(SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax")


def db() -> sqlite3.Connection:
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


def init_db() -> None:
    with db() as conn:
        conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                institution TEXT NOT NULL DEFAULT '',
                account_type TEXT NOT NULL DEFAULT 'BROKER',
                currency TEXT NOT NULL DEFAULT 'HKD',
                notes TEXT NOT NULL DEFAULT '',
                created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
            );
            CREATE TABLE IF NOT EXISTS instruments (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                symbol TEXT NOT NULL,
                name TEXT NOT NULL,
                market TEXT NOT NULL DEFAULT 'HK',
                currency TEXT NOT NULL DEFAULT 'HKD',
                category TEXT NOT NULL DEFAULT 'STOCK',
                UNIQUE(symbol, market)
            );
            CREATE TABLE IF NOT EXISTS transactions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
                instrument_id INTEGER NOT NULL REFERENCES instruments(id) ON DELETE RESTRICT,
                trade_date TEXT NOT NULL,
                txn_type TEXT NOT NULL,
                quantity REAL NOT NULL DEFAULT 0,
                price REAL NOT NULL DEFAULT 0,
                amount REAL NOT NULL DEFAULT 0,
                commission REAL NOT NULL DEFAULT 0,
                fee REAL NOT NULL DEFAULT 0,
                tax REAL NOT NULL DEFAULT 0,
                fx_rate REAL NOT NULL DEFAULT 1,
                ratio_from REAL NOT NULL DEFAULT 0,
                ratio_to REAL NOT NULL DEFAULT 0,
                note TEXT NOT NULL DEFAULT '',
                created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
            );
            CREATE INDEX IF NOT EXISTS ix_transactions_account_date ON transactions(account_id, trade_date, id);
            CREATE INDEX IF NOT EXISTS ix_transactions_instrument_date ON transactions(instrument_id, trade_date, id);
            """
        )


def row_dict(row: sqlite3.Row | None) -> dict | None:
    return dict(row) if row else None


def setting(key: str, default: str = "") -> str:
    with db() as conn:
        row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
    return row["value"] if row else default


def save_setting(key: str, value: str) -> None:
    with db() as conn:
        conn.execute(
            "INSERT INTO settings(key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
            (key, value),
        )


def as_decimal(value: object, default: str = "0") -> Decimal:
    try:
        return Decimal(str(value if value not in (None, "") else default))
    except Exception:
        return Decimal(default)


def money(value: Decimal | float | int) -> float:
    return float(as_decimal(value).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))


def quantity(value: Decimal | float | int) -> float:
    return float(as_decimal(value).quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP))


def require_login(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        if not session.get("ledger_authenticated"):
            return jsonify({"error": "請先登入本機帳房。"}), 401
        return fn(*args, **kwargs)
    return wrapped


def fetch_json(url: str, timeout: int = 12) -> dict:
    req = urllib.request.Request(url, headers={"User-Agent": "VictoriaLedger/1.0 (local personal tracker)"})
    with urllib.request.urlopen(req, timeout=timeout) as response:
        return json.loads(response.read().decode("utf-8"))


def normalized_symbol(symbol: str, market: str) -> str:
    symbol = symbol.strip().upper()
    if market == "HK" and "." not in symbol:
        return f"{symbol.zfill(4)}.HK"
    return symbol


def ledger_for(account_id: int | None = None, instrument_id: int | None = None) -> list[dict]:
    clauses, params = [], []
    if account_id:
        clauses.append("t.account_id = ?")
        params.append(account_id)
    if instrument_id:
        clauses.append("t.instrument_id = ?")
        params.append(instrument_id)
    where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
    sql = f"""
        SELECT t.*, a.name AS account_name, a.currency AS account_currency, a.account_type,
               i.symbol, i.name AS instrument_name, i.market, i.currency AS instrument_currency, i.category
          FROM transactions t
          JOIN accounts a ON a.id = t.account_id
          JOIN instruments i ON i.id = t.instrument_id
          {where}
         ORDER BY t.trade_date ASC, t.id ASC
    """
    with db() as conn:
        raw_rows = conn.execute(sql, params).fetchall()

    states: dict[tuple[int, int], dict[str, Decimal]] = {}
    result: list[dict] = []
    for raw in raw_rows:
        row = dict(raw)
        key = (row["account_id"], row["instrument_id"])
        state = states.setdefault(key, {"holding": Decimal("0"), "cost": Decimal("0"), "realized": Decimal("0"), "dividends": Decimal("0")})
        txn_type = row["txn_type"]
        qty = as_decimal(row["quantity"])
        price = as_decimal(row["price"])
        amount = as_decimal(row["amount"])
        fx = as_decimal(row["fx_rate"], "1")
        costs = (as_decimal(row["commission"]) + as_decimal(row["fee"]) + as_decimal(row["tax"])) * fx
        gross = (qty * price * fx) if txn_type not in {"DIVIDEND", "ADJUSTMENT"} else amount * fx
        realized_this = Decimal("0")
        dividend_this = Decimal("0")

        if txn_type in {"BUY", "MPF_CONTRIBUTION"}:
            state["holding"] += qty
            state["cost"] += gross + costs
        elif txn_type in {"SELL", "MPF_WITHDRAWAL"}:
            previous_holding = state["holding"]
            avg_before = state["cost"] / previous_holding if previous_holding else Decimal("0")
            cost_basis = qty * avg_before
            realized_this = (gross - costs) - cost_basis
            state["holding"] -= qty
            state["cost"] -= cost_basis
            state["realized"] += realized_this
        elif txn_type in {"SPLIT", "MERGE"}:
            if as_decimal(row["ratio_from"]) > 0 and as_decimal(row["ratio_to"]) > 0:
                state["holding"] *= as_decimal(row["ratio_to"]) / as_decimal(row["ratio_from"])
        elif txn_type == "DIVIDEND":
            dividend_this = amount * fx - costs
            state["dividends"] += dividend_this
        elif txn_type == "ADJUSTMENT":
            state["cost"] += amount * fx + costs

        if abs(state["holding"]) < Decimal("0.0000001"):
            state["holding"] = Decimal("0")
            state["cost"] = Decimal("0")
        avg_cost = state["cost"] / state["holding"] if state["holding"] else Decimal("0")
        row.update(
            {
                "gross_amount": money(gross),
                "total_costs": money(costs),
                "buy_quantity": quantity(qty) if txn_type in {"BUY", "MPF_CONTRIBUTION"} else 0,
                "sell_quantity": quantity(qty) if txn_type in {"SELL", "MPF_WITHDRAWAL"} else 0,
                "average_cost": money(avg_cost),
                "cumulative_holding": quantity(state["holding"]),
                "invested_cost": money(state["cost"]),
                "realized_this": money(realized_this),
                "realized_total": money(state["realized"]),
                "dividend_this": money(dividend_this),
                "dividend_total": money(state["dividends"]),
            }
        )
        result.append(row)
    return result


def positions() -> list[dict]:
    ledgers = ledger_for()
    grouped: dict[tuple[int, int], dict] = {}
    for item in ledgers:
        grouped[(item["account_id"], item["instrument_id"])] = item
    output = []
    for item in grouped.values():
        if item["cumulative_holding"] or item["realized_total"] or item["dividend_total"]:
            output.append(
                {
                    "account_id": item["account_id"], "account_name": item["account_name"], "account_type": item["account_type"],
                    "instrument_id": item["instrument_id"], "symbol": item["symbol"], "instrument_name": item["instrument_name"],
                    "market": item["market"], "currency": item["account_currency"], "category": item["category"],
                    "holding": item["cumulative_holding"], "average_cost": item["average_cost"], "invested_cost": item["invested_cost"],
                    "realized_total": item["realized_total"], "dividend_total": item["dividend_total"],
                }
            )
    return sorted(output, key=lambda x: (x["account_name"], x["symbol"]))


@app.get("/")
def home():
    return send_from_directory(STATIC_DIR, "index.html")


@app.get("/api/auth/status")
def auth_status():
    configured = bool(setting("password_hash"))
    return jsonify({"configured": configured, "authenticated": bool(session.get("ledger_authenticated"))})


@app.post("/api/auth/setup")
def auth_setup():
    if setting("password_hash"):
        return jsonify({"error": "本機密碼已設定，請直接登入。"}), 409
    payload = request.get_json(force=True)
    password = str(payload.get("password", ""))
    if len(password) < 8:
        return jsonify({"error": "請設定至少 8 個字元的本機密碼。"}), 400
    save_setting("password_hash", generate_password_hash(password))
    session["ledger_authenticated"] = True
    return jsonify({"ok": True})


@app.post("/api/auth/login")
def auth_login():
    password_hash = setting("password_hash")
    payload = request.get_json(force=True)
    if not password_hash or not check_password_hash(password_hash, str(payload.get("password", ""))):
        return jsonify({"error": "密碼不正確。"}), 401
    session["ledger_authenticated"] = True
    return jsonify({"ok": True})


@app.post("/api/auth/logout")
def auth_logout():
    session.clear()
    return jsonify({"ok": True})


@app.get("/api/dashboard")
@require_login
def dashboard():
    all_positions = positions()
    with db() as conn:
        accounts = [dict(x) for x in conn.execute("SELECT * FROM accounts ORDER BY name").fetchall()]
    return jsonify(
        {
            "accounts": accounts,
            "positions": all_positions,
            "summary": {
                "invested_cost": money(sum(as_decimal(x["invested_cost"]) for x in all_positions)),
                "realized_total": money(sum(as_decimal(x["realized_total"]) for x in all_positions)),
                "dividend_total": money(sum(as_decimal(x["dividend_total"]) for x in all_positions)),
                "open_positions": sum(1 for x in all_positions if as_decimal(x["holding"]) > 0),
            },
        }
    )


@app.route("/api/accounts", methods=["GET", "POST"])
@require_login
def accounts_api():
    if request.method == "GET":
        with db() as conn:
            rows = [dict(r) for r in conn.execute("SELECT * FROM accounts ORDER BY account_type, institution, name").fetchall()]
        return jsonify(rows)
    payload = request.get_json(force=True)
    name = str(payload.get("name", "")).strip()
    account_type = str(payload.get("account_type", "BROKER")).upper()
    if not name or account_type not in {"BROKER", "BANK", "MPF"}:
        return jsonify({"error": "請填寫帳戶名稱，並選擇有效類別。"}), 400
    with db() as conn:
        cur = conn.execute(
            "INSERT INTO accounts(name, institution, account_type, currency, notes) VALUES (?, ?, ?, ?, ?)",
            (name, str(payload.get("institution", "")).strip(), account_type, str(payload.get("currency", "HKD")).upper(), str(payload.get("notes", "")).strip()),
        )
    return jsonify({"id": cur.lastrowid}), 201


@app.route("/api/instruments", methods=["GET", "POST"])
@require_login
def instruments_api():
    if request.method == "GET":
        with db() as conn:
            rows = [dict(r) for r in conn.execute("SELECT * FROM instruments ORDER BY market, symbol").fetchall()]
        return jsonify(rows)
    payload = request.get_json(force=True)
    symbol = str(payload.get("symbol", "")).strip().upper()
    name = str(payload.get("name", "")).strip()
    market = str(payload.get("market", "HK")).upper()
    if not symbol or not name or market not in {"HK", "US", "MPF", "OTHER"}:
        return jsonify({"error": "請填寫代號、名稱及市場。"}), 400
    with db() as conn:
        try:
            cur = conn.execute(
                "INSERT INTO instruments(symbol, name, market, currency, category) VALUES (?, ?, ?, ?, ?)",
                (symbol, name, market, str(payload.get("currency", "HKD")).upper(), str(payload.get("category", "STOCK")).upper()),
            )
        except sqlite3.IntegrityError:
            return jsonify({"error": "此市場內已有相同代號。"}), 409
    return jsonify({"id": cur.lastrowid}), 201


@app.route("/api/transactions", methods=["GET", "POST"])
@require_login
def transactions_api():
    if request.method == "GET":
        account_id = request.args.get("account_id", type=int)
        instrument_id = request.args.get("instrument_id", type=int)
        return jsonify(ledger_for(account_id, instrument_id))

    payload = request.get_json(force=True)
    txn_type = str(payload.get("txn_type", "")).upper()
    account_id = int(payload.get("account_id", 0) or 0)
    instrument_id = int(payload.get("instrument_id", 0) or 0)
    trade_date = str(payload.get("trade_date", ""))
    qty = as_decimal(payload.get("quantity"))
    price = as_decimal(payload.get("price"))
    ratio_from, ratio_to = as_decimal(payload.get("ratio_from")), as_decimal(payload.get("ratio_to"))
    if txn_type not in ALLOWED_TRANSACTION_TYPES or not account_id or not instrument_id or len(trade_date) != 10:
        return jsonify({"error": "請完整填寫交易日期、帳戶、標的及交易類別。"}), 400
    if txn_type in {"BUY", "SELL", "MPF_CONTRIBUTION", "MPF_WITHDRAWAL"} and (qty <= 0 or price < 0):
        return jsonify({"error": "買入、賣出、供款及提取需使用正數數量及非負價格。"}), 400
    if txn_type in {"SPLIT", "MERGE"} and (ratio_from <= 0 or ratio_to <= 0):
        return jsonify({"error": "拆細／合併必須提供有效換股比例。"}), 400
    if txn_type in {"SELL", "MPF_WITHDRAWAL"}:
        current = next((x for x in positions() if x["account_id"] == account_id and x["instrument_id"] == instrument_id), None)
        if not current or as_decimal(current["holding"]) < qty:
            return jsonify({"error": "賣出／提取數量不能超過目前累算持股。"}), 400
    with db() as conn:
        cur = conn.execute(
            """INSERT INTO transactions(account_id, instrument_id, trade_date, txn_type, quantity, price, amount, commission, fee, tax, fx_rate, ratio_from, ratio_to, note)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (account_id, instrument_id, trade_date, txn_type, float(qty), float(price), float(as_decimal(payload.get("amount"))),
             float(as_decimal(payload.get("commission"))), float(as_decimal(payload.get("fee"))), float(as_decimal(payload.get("tax"))),
             float(as_decimal(payload.get("fx_rate"), "1")), float(ratio_from), float(ratio_to), str(payload.get("note", "")).strip()),
        )
    return jsonify({"id": cur.lastrowid}), 201


@app.delete("/api/transactions/<int:transaction_id>")
@require_login
def delete_transaction(transaction_id: int):
    with db() as conn:
        conn.execute("DELETE FROM transactions WHERE id = ?", (transaction_id,))
    return jsonify({"ok": True})


@app.get("/api/positions")
@require_login
def positions_api():
    account_id = request.args.get("account_id", type=int)
    rows = positions()
    if account_id:
        rows = [row for row in rows if row["account_id"] == account_id]
    return jsonify(rows)


@app.get("/api/market/quote")
@require_login
def market_quote():
    symbol = request.args.get("symbol", "").strip().upper()
    market = request.args.get("market", "US").upper()
    provider = setting("quote_provider", "yahoo")
    if not symbol:
        return jsonify({"error": "請輸入股票代號。"}), 400
    source_symbol = normalized_symbol(symbol, market)
    try:
        if provider == "alpha_vantage":
            api_key = setting("alpha_vantage_key")
            if not api_key:
                return jsonify({"error": "請先在設定頁輸入 Alpha Vantage API 金鑰。"}), 400
            url = "https://www.alphavantage.co/query?" + urllib.parse.urlencode({"function": "GLOBAL_QUOTE", "symbol": source_symbol, "apikey": api_key})
            data = fetch_json(url)
            quote = data.get("Global Quote", {})
            if not quote.get("05. price"):
                raise ValueError(data.get("Note") or data.get("Information") or "找不到此代號的報價")
            return jsonify({"symbol": source_symbol, "price": float(quote["05. price"]), "previous_close": float(quote.get("08. previous close", 0) or 0), "change_pct": float(str(quote.get("10. change percent", "0")).replace("%", "")), "currency": "", "as_of": quote.get("07. latest trading day", ""), "source": "Alpha Vantage"})
        url = f"https://query1.finance.yahoo.com/v8/finance/chart/{urllib.parse.quote(source_symbol)}?range=1d&interval=5m"
        chart = fetch_json(url).get("chart", {}).get("result", [])[0]
        meta = chart.get("meta", {}) if chart else {}
        if not meta.get("regularMarketPrice"):
            raise ValueError("資料供應商沒有回傳有效報價")
        return jsonify({"symbol": source_symbol, "price": float(meta.get("regularMarketPrice", 0)), "previous_close": float(meta.get("previousClose", 0) or 0), "change_pct": float(meta.get("regularMarketPrice", 0) / meta.get("previousClose", 1) * 100 - 100) if meta.get("previousClose") else 0, "currency": meta.get("currency", ""), "as_of": datetime.now(timezone.utc).isoformat(), "source": "Yahoo Finance（延時以供應商回傳為準）"})
    except Exception as exc:
        return jsonify({"error": f"暫時未能取得報價：{str(exc)}"}), 502


@app.get("/api/market/history")
@require_login
def market_history():
    symbol = request.args.get("symbol", "").strip().upper()
    market = request.args.get("market", "US").upper()
    period = request.args.get("period", "3mo")
    if not symbol:
        return jsonify({"error": "請輸入股票代號。"}), 400
    source_symbol = normalized_symbol(symbol, market)
    try:
        url = f"https://query1.finance.yahoo.com/v8/finance/chart/{urllib.parse.quote(source_symbol)}?range={urllib.parse.quote(period)}&interval=1d"
        chart = fetch_json(url).get("chart", {}).get("result", [])[0]
        closes = chart.get("indicators", {}).get("quote", [{}])[0].get("close", [])
        dates = [datetime.fromtimestamp(t, timezone.utc).strftime("%Y-%m-%d") for t in chart.get("timestamp", [])]
        points = [{"date": d, "close": c} for d, c in zip(dates, closes) if c is not None]
        return jsonify({"symbol": source_symbol, "points": points, "source": "Yahoo Finance"})
    except Exception as exc:
        return jsonify({"error": f"暫時未能取得圖表資料：{str(exc)}"}), 502


@app.get("/api/news")
@require_login
def news_api():
    query = request.args.get("query", "香港股票 市場").strip()
    url = "https://news.google.com/rss/search?" + urllib.parse.urlencode({"q": query, "hl": "zh-HK", "gl": "HK", "ceid": "HK:zh-Hant"})
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "VictoriaLedger/1.0"})
        with urllib.request.urlopen(req, timeout=12) as response:
            root = ET.fromstring(response.read())
        items = []
        for item in root.findall("./channel/item")[:8]:
            source = item.find("source")
            items.append({"title": item.findtext("title", ""), "link": item.findtext("link", ""), "published": item.findtext("pubDate", ""), "source": source.text if source is not None else "Google News"})
        return jsonify({"query": query, "items": items, "source": "Google News RSS"})
    except Exception as exc:
        return jsonify({"error": f"暫時未能取得新聞：{str(exc)}"}), 502


@app.get("/api/settings")
@require_login
def get_settings():
    return jsonify({"quote_provider": setting("quote_provider", "yahoo"), "alpha_vantage_key_configured": bool(setting("alpha_vantage_key"))})


@app.put("/api/settings")
@require_login
def update_settings():
    payload = request.get_json(force=True)
    provider = str(payload.get("quote_provider", "yahoo"))
    if provider not in {"yahoo", "alpha_vantage"}:
        return jsonify({"error": "不支援的報價供應商。"}), 400
    save_setting("quote_provider", provider)
    if str(payload.get("alpha_vantage_key", "")).strip():
        save_setting("alpha_vantage_key", str(payload["alpha_vantage_key"]).strip())
    return jsonify({"ok": True})


@app.post("/api/cost/estimate")
@require_login
def estimate_cost():
    payload = request.get_json(force=True)
    market = str(payload.get("market", "HK")).upper()
    side = str(payload.get("side", "BUY")).upper()
    gross = as_decimal(payload.get("quantity")) * as_decimal(payload.get("price"))
    commission_rate = as_decimal(payload.get("commission_rate")) / Decimal("100")
    commission_min = as_decimal(payload.get("commission_min"))
    commission = max(gross * commission_rate, commission_min) if gross else Decimal("0")
    manual_fee = as_decimal(payload.get("manual_fee"))
    details: dict[str, Decimal] = {"broker_commission": commission, "broker_or_other_fee": manual_fee}
    if market == "HK":
        details.update({
            "stamp_duty": (gross * Decimal("0.001")).to_integral_value(rounding=ROUND_CEILING),
            "sfc_levy": (gross * Decimal("0.000027")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "afrc_levy": (gross * Decimal("0.0000015")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
            "hkex_trading_fee": (gross * Decimal("0.0000565")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP),
        })
    elif side == "SELL":
        details["regulatory_fee"] = as_decimal(payload.get("regulatory_fee"))
    total = sum(details.values(), Decimal("0"))
    basis_note = (
        "港股法定徵費按 HKEX 公開交易費率計算；佣金及券商代收費請按你的成交單覆寫。"
        if market == "HK"
        else "美股佣金、平台費及監管費會因券商、成交場所與日期而異；請按你的券商成交單或收費表輸入及核對。"
    )
    return jsonify({"gross": money(gross), "details": {k: money(v) for k, v in details.items()}, "total_cost": money(total), "net_cash": money(gross + total if side == "BUY" else gross - total), "basis_note": basis_note})


@app.get("/api/backup")
@require_login
def backup_db():
    from flask import send_file
    return send_file(DB_PATH, as_attachment=True, download_name=f"victoria-ledger-{datetime.now().strftime('%Y%m%d')}.db")


if __name__ == "__main__":
    init_db()
    # 直接在個人電腦執行時只監聽 localhost；受管預覽傳入 PORT 時才對預覽代理公開。
    ledger_port = int(os.getenv("LEDGER_PORT") or os.getenv("PORT") or "8787")
    ledger_host = os.getenv("LEDGER_HOST") or ("0.0.0.0" if os.getenv("PORT") else "127.0.0.1")
    print(f"維港帳房已啟動：http://{ledger_host}:{ledger_port}")
    app.run(host=ledger_host, port=ledger_port, debug=False)
else:
    init_db()
