import hmac
import logging
from functools import wraps

from flask import Blueprint, current_app, request

from app import db
from app.auth import ensure_fresh_token
from app.models import Account, ScheduledPost
from app.services.errors import ValidationError
from app.services.insights import top_videos as collect_top_videos
from app.services.posts import publish_now, validate_post_fields
from app.services.scheduled import create_scheduled_post, delete_scheduled_post, reschedule_post, retry_post
from app.tiktok_api import (
    TikTokAPIError, fetch_publish_status, get_user_info, list_videos,
)

api_bp = Blueprint("api", __name__, url_prefix="/api/v1")

log = logging.getLogger(__name__)


def api_error(message, code="ERROR", status=400):
    return {"success": False, "data": None, "error": {"code": code, "message": message}}, status


def api_success(data, status=200):
    return {"success": True, "data": data, "error": None}, status


def api_key_required(f):
    """API guard: a constant-time comparison of the X-API-Key (or Bearer) header against API_KEY."""
    @wraps(f)
    def decorated(*args, **kwargs):
        api_key = current_app.config.get("API_KEY", "")
        if not api_key:
            return api_error("API key not configured. Set API_KEY in .env", "API_NOT_CONFIGURED", 503)
        provided = request.headers.get("X-API-Key") or ""
        if not provided:
            auth = request.headers.get("Authorization", "")
            if auth.startswith("Bearer "):
                provided = auth[7:]
        if not provided or not hmac.compare_digest(provided, api_key):
            return api_error("Invalid or missing API key.", "UNAUTHORIZED", 401)
        return f(*args, **kwargs)
    return decorated


def resolve_account(require_explicit=False):
    """Pick the account to act on: `account_id` query param or `X-Account-Id` header (database id
    or open_id). Reads fall back to the first connected account; mutations must be explicit.
    Returns (account, error_response)."""
    value = request.args.get("account_id") or request.headers.get("X-Account-Id")
    if value:
        account = None
        try:
            account = db.session.get(Account, int(value))
        except (TypeError, ValueError):
            pass
        if account is None:
            account = Account.query.filter_by(open_id=value).first()
        if not account:
            return None, api_error("No account matches the given account_id.", "NO_ACCOUNT", 404)
        return account, None
    if require_explicit:
        return None, api_error(
            "This is a mutating endpoint: pass account_id (query param) or X-Account-Id (header).",
            "EXPLICIT_ACCOUNT_REQUIRED", 400,
        )
    account = Account.query.order_by(Account.display_name).first()
    if not account:
        return None, api_error("No account connected. Connect an account via the web UI first.", "NO_ACCOUNT", 404)
    return account, None


def tiktok_error_response(e):
    """Map a TikTokAPIError to the envelope with its client-safe message."""
    if e.status == 404:
        return api_error(e.message, "NOT_FOUND", 404)
    if e.status in (401, 403):
        return api_error(e.message, "FORBIDDEN", 403)
    if e.status == 429:
        return api_error(e.message, "RATE_LIMITED", 429)
    return api_error(e.message, "UPSTREAM_ERROR", 502)


def _json_body():
    return request.get_json(silent=True) or {}


# =====================
# Health
# =====================

@api_bp.route("/health")
@api_key_required
def health():
    return api_success({"status": "ok", "version": "1.0"})


# =====================
# Accounts
# =====================

@api_bp.route("/accounts", methods=["GET"])
@api_key_required
def api_list_accounts():
    accounts = Account.query.order_by(Account.display_name).all()
    return api_success([a.to_dict() for a in accounts])


@api_bp.route("/accounts/<int:account_db_id>", methods=["GET"])
@api_key_required
def api_get_account(account_db_id):
    account = db.session.get(Account, account_db_id)
    if not account:
        return api_error("Account not found.", "NOT_FOUND", 404)
    return api_success(account.to_dict())


@api_bp.route("/accounts/<int:account_db_id>/refresh", methods=["POST"])
@api_key_required
def api_refresh_account(account_db_id):
    account = db.session.get(Account, account_db_id)
    if not account:
        return api_error("Account not found.", "NOT_FOUND", 404)
    try:
        token = ensure_fresh_token(account)
        user = get_user_info(token) or {}
        if user:
            account.display_name = user.get("display_name") or account.display_name
            account.avatar_url = user.get("avatar_url") or account.avatar_url
            for field in ("follower_count", "following_count", "likes_count", "video_count"):
                if user.get(field) is not None:
                    setattr(account, field, int(user[field]))
            db.session.commit()
        return api_success(account.to_dict())
    except TikTokAPIError as e:
        return tiktok_error_response(e)


@api_bp.route("/accounts/<int:account_db_id>", methods=["DELETE"])
@api_key_required
def api_delete_account(account_db_id):
    account = db.session.get(Account, account_db_id)
    if not account:
        return api_error("Account not found.", "NOT_FOUND", 404)
    name = account.display_name
    db.session.delete(account)
    db.session.commit()
    return api_success({"deleted": True, "name": name})


# =====================
# Videos (published)
# =====================

@api_bp.route("/videos", methods=["GET"])
@api_key_required
def api_list_videos():
    account, err = resolve_account()
    if err:
        return err
    max_count = min(max(int(request.args.get("max_count", "20")), 1), 20)
    try:
        cursor = int(request.args.get("cursor", "0"))
    except (TypeError, ValueError):
        cursor = 0
    try:
        token = ensure_fresh_token(account)
        result = list_videos(token, max_count=max_count, cursor=cursor)
        return api_success(result)
    except TikTokAPIError as e:
        return tiktok_error_response(e)


# =====================
# Posts — publish now (explicit account required)
# =====================

@api_bp.route("/posts", methods=["POST"])
@api_key_required
def api_create_post():
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    data = _json_body()
    try:
        token = ensure_fresh_token(account)
        fields = validate_post_fields(data, account.scopes)
        result = publish_now(token, fields, current_app.config["UPLOAD_FOLDER"])
        return api_success(result, status=201)
    except ValidationError as e:
        return api_error(e.message, "INVALID_INPUT", 400)
    except TikTokAPIError as e:
        return tiktok_error_response(e)


@api_bp.route("/posts/media", methods=["POST"])
@api_key_required
def api_create_media_post():
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    video_file = request.files.get("video_file") or request.files.get("file")
    if not video_file or not video_file.filename:
        return api_error("No video file provided. Use multipart field 'video_file'.", "NO_FILE", 400)
    data = request.form.to_dict()
    data["video_file"] = video_file
    try:
        token = ensure_fresh_token(account)
        fields = validate_post_fields(data, account.scopes)
        result = publish_now(token, fields, current_app.config["UPLOAD_FOLDER"])
        return api_success(result, status=201)
    except ValidationError as e:
        return api_error(e.message, "INVALID_INPUT", 400)
    except TikTokAPIError as e:
        return tiktok_error_response(e)


@api_bp.route("/posts/<publish_id>/status", methods=["GET"])
@api_key_required
def api_post_status(publish_id):
    account, err = resolve_account()
    if err:
        return err
    try:
        token = ensure_fresh_token(account)
        status = fetch_publish_status(token, publish_id)
        return api_success({"publish_id": publish_id, **status})
    except TikTokAPIError as e:
        return tiktok_error_response(e)


# =====================
# Scheduled posts (ADR-005)
# =====================

@api_bp.route("/scheduled", methods=["GET"])
@api_key_required
def api_list_scheduled():
    account, err = resolve_account()
    if err:
        return err
    posts = ScheduledPost.query.filter_by(account_id=account.id).order_by(ScheduledPost.scheduled_at).all()
    return api_success([p.to_dict() for p in posts])


@api_bp.route("/scheduled", methods=["POST"])
@api_key_required
def api_create_scheduled():
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    data = _json_body()
    try:
        post = create_scheduled_post(
            account, data, current_app.config["MEDIA_FOLDER"],
            upload_max_bytes=current_app.config["MAX_CONTENT_LENGTH"],
        )
        return api_success(post.to_dict(), status=201)
    except ValidationError as e:
        return api_error(e.message, "INVALID_INPUT", 400)


@api_bp.route("/scheduled/media", methods=["POST"])
@api_key_required
def api_create_scheduled_media():
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    video_file = request.files.get("video_file") or request.files.get("file")
    if not video_file or not video_file.filename:
        return api_error("No video file provided. Use multipart field 'video_file'.", "NO_FILE", 400)
    data = request.form.to_dict()
    data["video_file"] = video_file
    try:
        post = create_scheduled_post(
            account, data, current_app.config["MEDIA_FOLDER"],
            upload_max_bytes=current_app.config["MAX_CONTENT_LENGTH"],
        )
        return api_success(post.to_dict(), status=201)
    except ValidationError as e:
        return api_error(e.message, "INVALID_INPUT", 400)


@api_bp.route("/scheduled/<int:post_id>", methods=["PATCH", "PUT"])
@api_key_required
def api_reschedule_scheduled(post_id):
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    post = db.session.get(ScheduledPost, post_id)
    if not post or post.account_id != account.id:
        return api_error("Scheduled post not found for this account.", "NOT_FOUND", 404)
    data = _json_body()
    try:
        post = reschedule_post(post, data.get("scheduled_at"))
        return api_success(post.to_dict())
    except ValidationError as e:
        return api_error(e.message, "INVALID_INPUT", 400)


@api_bp.route("/scheduled/<int:post_id>/retry", methods=["POST"])
@api_key_required
def api_retry_scheduled(post_id):
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    post = db.session.get(ScheduledPost, post_id)
    if not post or post.account_id != account.id:
        return api_error("Scheduled post not found for this account.", "NOT_FOUND", 404)
    if post.status != ScheduledPost.STATUS_FAILED:
        return api_error("Only failed posts can be retried.", "INVALID_INPUT", 400)
    retry_post(post)
    return api_success(post.to_dict())


@api_bp.route("/scheduled/<int:post_id>", methods=["DELETE"])
@api_key_required
def api_delete_scheduled(post_id):
    account, err = resolve_account(require_explicit=True)
    if err:
        return err
    post = db.session.get(ScheduledPost, post_id)
    if not post or post.account_id != account.id:
        return api_error("Scheduled post not found for this account.", "NOT_FOUND", 404)
    delete_scheduled_post(post)
    return api_success({"deleted": True, "id": post_id})


# =====================
# Insights
# =====================

@api_bp.route("/insights/account", methods=["GET"])
@api_key_required
def api_account_insights():
    account, err = resolve_account()
    if err:
        return err
    try:
        token = ensure_fresh_token(account)
        user = get_user_info(token) or {}
        return api_success({"open_id": account.open_id, "user": user})
    except TikTokAPIError as e:
        return tiktok_error_response(e)


@api_bp.route("/insights/videos", methods=["GET"])
@api_key_required
def api_top_videos():
    account, err = resolve_account()
    if err:
        return err
    limit = min(max(int(request.args.get("limit", "10")), 1), 50)
    try:
        token = ensure_fresh_token(account)
        result = collect_top_videos(token, limit=limit)
        return api_success({
            "open_id": account.open_id,
            "videos": result["videos"],
            "total_views": result["total_views"],
            "counted": result["counted"],
        })
    except TikTokAPIError as e:
        return tiktok_error_response(e)
