#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""公開系統頁面與健康檢查路由。

此 blueprint 無 url_prefix，保留外部監控與既有前端使用的公開 URL。
"""

import os
import zipfile
from datetime import datetime, timezone, timedelta
from urllib.parse import urlparse

from flask import Blueprint, Response, jsonify, render_template, send_from_directory, url_for
from sqlalchemy import text

from auth import login_required
from config import BASE_DIR, DATABASE_TYPE, GRIST_URL, METABASE_URL, SYSTEM_VERSION
from database.manager import DatabaseManager
from database.models import Product, PriceRecord
from services.json_storage import load_categories
from services.logger_manager import SystemLogger
from utils.security import safe_join


system_public_bp = Blueprint('system_public', __name__)

sys_log = SystemLogger("SystemPublicRoutes").get_logger()
TAIPEI_TZ = timezone(timedelta(hours=8))
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = os.getenv('PUBLIC_URL', '服務啟動中...')
STATIC_DIR = os.path.join(BASE_DIR, 'web/static')


def _safe_launch_url(configured_url, bridge_path):
    """Return a browser-safe launch URL, keeping nav links inside momo-pro by default."""
    candidate = (configured_url or '').strip()
    if not candidate or candidate.rstrip('/') == bridge_path.rstrip('/'):
        return ''
    if candidate.startswith('/') and not candidate.startswith('//'):
        return candidate

    parsed = urlparse(candidate)
    if parsed.scheme in {'http', 'https'} and parsed.hostname == 'mo.wooo.work':
        if parsed.path.rstrip('/') == bridge_path.rstrip('/'):
            return ''
        return candidate
    return ''


def _configured_url_label(configured_url):
    candidate = (configured_url or '').strip()
    if not candidate:
        return '未設定'
    parsed = urlparse(candidate)
    if parsed.scheme in {'http', 'https'} and parsed.hostname != 'mo.wooo.work':
        return f'{parsed.hostname}（已由入口攔截）'
    return candidate


def _is_blocked_external_url(configured_url):
    candidate = (configured_url or '').strip()
    if not candidate or candidate.startswith('/') and not candidate.startswith('//'):
        return False
    parsed = urlparse(candidate)
    return parsed.scheme in {'http', 'https'} and parsed.hostname != 'mo.wooo.work'


def _external_tool_payload(kind):
    if kind == 'metabase':
        launch_url = _safe_launch_url(METABASE_URL, '/metabase')
        return {
            'key': 'metabase',
            'eyebrow': 'Analytics Bridge',
            'title': '自訂圖表入口',
            'status_label': '待接上 BI 服務' if not launch_url else '可開啟',
            'summary': '這裡是 momo-pro 的 Metabase 安全入口。導覽先停在系統內部，不再出現空白頁或錯站外跳。',
            'detail': '目前正式主機沒有啟用 Metabase / Grist 的 bi profile 容器，Gateway 也沒有可用 proxy；因此先提供可用的分析替代入口與設定診斷。',
            'launch_href': launch_url,
            'launch_label': '開啟 Metabase',
            'configured_label': _configured_url_label(METABASE_URL),
            'checks': [
                {'label': '導覽路由', 'value': '/metabase', 'state': 'ok'},
                {'label': '跨站保護', 'value': '已鎖在 momo-pro', 'state': 'ok'},
                {'label': 'BI 服務', 'value': '尚未接入 proxy', 'state': 'warn'},
            ],
            'actions': [
                {'label': '月份總表', 'href': '/monthly_summary_analysis', 'icon': 'fas fa-table'},
                {'label': '成長分析', 'href': '/growth_analysis', 'icon': 'fas fa-arrow-trend-up'},
                {'label': '當日業績', 'href': '/daily_sales', 'icon': 'fas fa-calendar-day'},
            ],
        }

    launch_url = _safe_launch_url(GRIST_URL, '/grist')
    blocked_external = _is_blocked_external_url(GRIST_URL)
    return {
        'key': 'grist',
        'eyebrow': 'Data Collaboration',
        'title': '資料協作入口',
        'status_label': '錯鏈已攔截' if blocked_external else ('可開啟' if launch_url else '待接上協作服務'),
        'summary': '資料協作入口已回到 momo-pro 內部，不會再連到其他專案站台。',
        'detail': 'Grist 的正式 momo-pro 專案隔離尚未接上 Gateway；在完成專屬服務前，本頁會提供資料作業入口與目前設定狀態。',
        'launch_href': launch_url,
        'launch_label': '開啟 Grist',
        'configured_label': _configured_url_label(GRIST_URL),
        'checks': [
            {'label': '導覽路由', 'value': '/grist', 'state': 'ok'},
            {'label': '錯站攔截', 'value': '已攔截外部錯鏈' if blocked_external else '未偵測錯鏈', 'state': 'ok'},
            {'label': '協作服務', 'value': '尚未接入 proxy', 'state': 'warn'},
        ],
        'actions': [
            {'label': '月份總表', 'href': '/monthly_summary_analysis', 'icon': 'fas fa-table'},
            {'label': '雲端匯入', 'href': '/auto_import', 'icon': 'fas fa-download'},
            {'label': '業績分析', 'href': '/sales_analysis', 'icon': 'fas fa-chart-bar'},
        ],
    }


@system_public_bp.route('/favicon.ico')
def favicon():
    """使用既有品牌圖示回應瀏覽器預設 favicon 探測，避免全站 404 噪音。"""
    return send_from_directory(
        os.path.join(STATIC_DIR, 'images'),
        'logo_circle.svg',
        mimetype='image/svg+xml',
        max_age=604800,
    )


@system_public_bp.route('/health')
def health_check():
    """健康檢查端點 - 供 Nginx 和 Docker healthcheck 使用"""
    try:
        return jsonify({
            'status': 'healthy',
            'database': DATABASE_TYPE,
            'version': SYSTEM_VERSION
        }), 200
    except Exception as e:
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 500


@system_public_bp.route('/metabase')
@system_public_bp.route('/metabase/')
@login_required
def metabase_status():
    """Internal status page for the BI entrypoint when the public proxy is not attached."""
    return render_template(
        'external_tool_status.html',
        active_page='metabase',
        system_version=SYSTEM_VERSION,
        tool=_external_tool_payload('metabase'),
    )


@system_public_bp.route('/grist')
@system_public_bp.route('/grist/')
@login_required
def grist_status():
    """Internal status page for the data collaboration entrypoint."""
    return render_template(
        'external_tool_status.html',
        active_page='grist',
        system_version=SYSTEM_VERSION,
        tool=_external_tool_payload('grist'),
    )


@system_public_bp.route('/metrics')
def prometheus_metrics():
    """Prometheus 指標端點 - 供 Prometheus 抓取監控資料"""
    try:
        from prometheus_client import generate_latest, CONTENT_TYPE_LATEST, Gauge, CollectorRegistry
        from services.ai_automation_metrics import snapshot as ai_metrics_snapshot

        registry = CollectorRegistry()

        app_info = Gauge('momo_app_info', '應用程式資訊', ['version', 'database_type'], registry=registry)
        app_info.labels(version=SYSTEM_VERSION, database_type=DATABASE_TYPE).set(1)

        app_health = Gauge('momo_app_health', '應用程式健康狀態', registry=registry)
        db_status = Gauge('momo_database_up', '資料庫連線狀態', registry=registry)

        try:
            db = DatabaseManager()
            with db.engine.connect() as conn:
                conn.execute(text("SELECT 1"))
            db_status.set(1)
            app_health.set(1)
        except Exception:
            db_status.set(0)
            app_health.set(0)

        session = None
        try:
            db = DatabaseManager()
            session = db.get_session()
            _set_database_record_counts(registry, Gauge, session)
        except Exception as e:
            sys_log.warning(f"[Metrics] 無法取得資料庫統計: {e}")
        finally:
            if session is not None:
                session.close()

        try:
            _register_ai_automation_metrics(registry, Gauge, ai_metrics_snapshot())
        except Exception as e:
            sys_log.warning(f"[Metrics] 無法取得 AI 自動化指標: {e}")

        return Response(generate_latest(registry), mimetype=CONTENT_TYPE_LATEST)

    except ImportError:
        metrics_text = f"""# HELP momo_app_health 應用程式健康狀態
# TYPE momo_app_health gauge
momo_app_health 1
# HELP momo_app_info 應用程式資訊
# TYPE momo_app_info gauge
momo_app_info{{version="{SYSTEM_VERSION}",database_type="{DATABASE_TYPE}"}} 1
"""
        return Response(metrics_text, mimetype='text/plain; charset=utf-8')
    except Exception as e:
        sys_log.error(f"[Metrics] 指標生成錯誤: {e}")
        return Response(f"# Error: {e}\n", mimetype='text/plain; charset=utf-8'), 500


def _labels_to_dict(labels):
    return dict(labels)


def _set_database_record_counts(registry, gauge_cls, session):
    """Register DB counts without selecting drift-prone sales columns."""
    product_count = gauge_cls('momo_products_total', '商品總數', registry=registry)
    product_count.set(session.query(Product).count())

    price_record_count = gauge_cls('momo_price_records_total', '價格記錄總數', registry=registry)
    price_record_count.set(session.query(PriceRecord).count())

    # V-Fix: realtime_sales_monthly 線上欄位曾與 ORM 不同步，metrics 只需要總筆數。
    sales_count = gauge_cls('momo_sales_records_total', '業績資料總數', registry=registry)
    sales_total = session.execute(text("SELECT COUNT(*) FROM realtime_sales_monthly")).scalar() or 0
    sales_count.set(sales_total)


def _register_ai_automation_metrics(registry, gauge_cls, metrics_snapshot):
    """Export dependency-free AI metrics into a per-request Prometheus registry."""
    gauges = {}

    def get_gauge(name, help_text, label_names):
        if name not in gauges:
            gauges[name] = gauge_cls(name, help_text, label_names, registry=registry)
        return gauges[name]

    definitions = {
        "event_router_dispatch_total": (
            "momo_ai_event_router_dispatch_total",
            "EventRouter dispatch count",
            ["event_type", "outcome", "tier"],
        ),
        "event_router_safe_action_total": (
            "momo_ai_event_router_safe_action_total",
            "EventRouter L2 safe action count",
            ["action", "status"],
        ),
        "event_router_replay_total": (
            "momo_ai_event_router_replay_total",
            "EventRouter queued Telegram replay count",
            ["status"],
        ),
        "autoheal_action_total": (
            "momo_ai_autoheal_action_total",
            "AutoHeal action count",
            ["action", "error_type", "result"],
        ),
    }

    counter_samples = {
        ("event_router_dispatch_total", (
            ("event_type", "baseline"),
            ("outcome", "none"),
            ("tier", "baseline"),
        )): 0,
        ("event_router_safe_action_total", (
            ("action", "baseline"),
            ("status", "none"),
        )): 0,
        ("event_router_replay_total", (
            ("status", "none"),
        )): 0,
        ("autoheal_action_total", (
            ("action", "baseline"),
            ("error_type", "none"),
            ("result", "none"),
        )): 0,
    }
    counter_samples.update(metrics_snapshot.get("counters", {}))

    for (metric, labels), value in counter_samples.items():
        if metric not in definitions:
            continue
        name, help_text, label_names = definitions[metric]
        gauge = get_gauge(name, help_text, label_names)
        label_values = _labels_to_dict(labels)
        gauge.labels(**{name: label_values.get(name, "unknown") for name in label_names}).set(value)

    latency_defs = {
        "event_router_latency_ms": (
            "momo_ai_event_router_latency_ms",
            "EventRouter dispatch latency in milliseconds",
            ["event_type", "tier"],
        ),
        "autoheal_duration_ms": (
            "momo_ai_autoheal_duration_ms",
            "AutoHeal action duration in milliseconds",
            ["action", "error_type"],
        ),
    }

    latency_samples = {
        ("event_router_latency_ms", (
            ("event_type", "baseline"),
            ("tier", "baseline"),
        )): {"count": 0, "sum": 0, "max": 0},
        ("autoheal_duration_ms", (
            ("action", "baseline"),
            ("error_type", "none"),
        )): {"count": 0, "sum": 0, "max": 0},
    }
    latency_samples.update(metrics_snapshot.get("latency", {}))

    for (metric, labels), values in latency_samples.items():
        if metric not in latency_defs:
            continue
        name, help_text, label_names = latency_defs[metric]
        label_values = _labels_to_dict(labels)
        for suffix in ("count", "sum", "max"):
            gauge = get_gauge(f"{name}_{suffix}", f"{help_text} ({suffix})", label_names)
            gauge.labels(**{name: label_values.get(name, "unknown") for name in label_names}).set(values.get(suffix, 0))


@system_public_bp.route('/settings')
def settings():
    """分類設定頁面"""
    categories = load_categories()
    return render_template('settings.html',
                           categories=categories,
                           public_url=public_url,
                           system_version=SYSTEM_VERSION)


@system_public_bp.route('/system_settings')
def system_settings_page():
    """系統設定與匯入頁面"""
    return render_template(
        'system_settings.html',
        system_version=SYSTEM_VERSION,
        active_page='system_settings',
    )


@system_public_bp.route('/ai_automation_smoke')
@login_required
def ai_automation_smoke_page():
    """AI 自動化閉環 smoke dashboard."""
    return render_template(
        'ai_automation_smoke.html',
        system_version=SYSTEM_VERSION,
        active_page='ai_automation_smoke',
    )


@system_public_bp.route('/api/ai-automation/smoke')
@login_required
def ai_automation_smoke_api():
    """Read-only smoke status for the four-agent AI automation control plane."""
    from services.ai_automation_smoke_service import collect_ai_automation_smoke
    return jsonify(collect_ai_automation_smoke())


@system_public_bp.route('/api/ai-automation/smoke/history/export')
@login_required
def ai_automation_smoke_history_export():
    """Export compact smoke history JSONL."""
    from services.ai_automation_smoke_service import export_smoke_history_jsonl
    export = export_smoke_history_jsonl()
    response = Response(export["content"], mimetype='application/x-ndjson; charset=utf-8')
    response.headers["Content-Disposition"] = "attachment; filename=ai_automation_smoke_history.jsonl"
    response.headers["X-Smoke-History-Count"] = str(export["count"])
    return response


@system_public_bp.route('/api/ai-automation/smoke/history/clear', methods=['POST'])
@login_required
def ai_automation_smoke_history_clear():
    """Clear local compact smoke history JSONL."""
    from services.ai_automation_smoke_service import clear_smoke_history
    return jsonify(clear_smoke_history())


@system_public_bp.route('/api/ai-automation/smoke/daily-summary/send', methods=['POST'])
@login_required
def ai_automation_smoke_daily_summary_send():
    """Send compact smoke trend daily summary to Telegram."""
    from services.ai_automation_smoke_service import send_smoke_daily_summary
    result = send_smoke_daily_summary()
    status_code = 200 if result.get("status") == "sent" else 502
    return jsonify(result), status_code


@system_public_bp.route('/logs')
def show_logs():
    return render_template('logs.html', active_page='logs')


@system_public_bp.route('/api/logs')
def get_logs_api():
    if os.path.exists(LOG_FILE_PATH):
        try:
            with open(LOG_FILE_PATH, 'r', encoding='utf-8') as f:
                return jsonify({"logs": "".join(f.readlines()[-60:])})
        except Exception as e:
            sys_log.error(f"[Web] [Logs] ❌ 日誌 API 讀取異常 | Error: {e}")
            return jsonify({"logs": "讀取日誌異常"})
    return jsonify({"logs": "等待系統啟動中..."})


@system_public_bp.route('/api/backup', methods=['POST'])
@login_required
def trigger_backup():
    """API: 觸發系統完整備份"""
    try:
        sys_log.info("[System] [Backup] 💾 開始執行系統完整備份...")
        backup_dir = os.path.join(BASE_DIR, 'backups')
        if not os.path.exists(backup_dir):
            os.makedirs(backup_dir)

        timestamp = datetime.now(TAIPEI_TZ).strftime('%Y%m%d_%H%M')
        zip_filename = f"momo_system_backup_{SYSTEM_VERSION}_{timestamp}.zip"
        zip_filepath = os.path.join(backup_dir, zip_filename)

        with zipfile.ZipFile(zip_filepath, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for root, dirs, files in os.walk(BASE_DIR):
                dirs[:] = [d for d in dirs if d not in ['backups', '__pycache__', 'venv', '.git', '.idea', '.vscode', 'node_modules']]

                for file in files:
                    if file == zip_filename:
                        continue
                    if file.endswith('.pyc') or file.endswith('.DS_Store'):
                        continue

                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, BASE_DIR)
                    zipf.write(file_path, arcname)

        sys_log.info(f"[System] [Backup] ✅ 系統備份完成 | File: {zip_filename}")

        download_url = url_for('system_public.download_backup', filename=zip_filename)

        return jsonify({
            "status": "success",
            "message": f"備份成功！\n檔案已儲存為: {zip_filename}\n即將開始下載...",
            "download_url": download_url
        })
    except Exception as e:
        sys_log.error(f"[System] [Backup] ❌ 備份失敗 | Error: {e}")
        return jsonify({"status": "error", "message": str(e)}), 500


@system_public_bp.route('/api/backup/download/<path:filename>')
@login_required
def download_backup(filename):
    """API: 下載備份檔案（已加入路徑遍歷防護）"""
    try:
        backup_dir = os.path.join(BASE_DIR, 'backups')
        safe_path = safe_join(backup_dir, filename)

        if not safe_path.exists():
            sys_log.warning(f"[Security] 備份檔案不存在 | File: {filename}")
            return jsonify({'error': '檔案不存在'}), 404

        if not safe_path.is_file():
            sys_log.warning(f"[Security] 嘗試下載非檔案路徑 | Path: {filename}")
            return jsonify({'error': '非法路徑'}), 400

        return send_from_directory(backup_dir, safe_path.name, as_attachment=True)

    except ValueError as e:
        sys_log.error(f"[Security] 路徑遍歷攻擊嘗試被阻擋 | Filename: {filename} | Error: {e}")
        return jsonify({'error': '非法路徑'}), 400
    except Exception as e:
        sys_log.error(f"[System] 下載備份失敗 | Error: {e}")
        return jsonify({'error': '下載失敗'}), 500
