"""
API Subdomain Routes  —  api.<yourdomain>
Handles all REST API requests authenticated via user API keys.
Supports restriction_type: 'all' | 'ip' | 'domain'
"""
import os
import json
import sqlite3
import uuid
import datetime

dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))

from werkzeug.utils import secure_filename
from flask import (
    request, jsonify, send_file, abort, g, current_app, redirect
)


# ── Allowed file types ────────────────────────────────────────────────────────
API_ALLOWED_FILETYPES = [
    "image", "audio", "video", "document", "device",
    "archive", "ebook", "pdf", "hash",
    "image-compressor", "video-compressor", "document-compressor",
]


# ── DB helper ─────────────────────────────────────────────────────────────────
def _db():
    path = os.path.join(dir_path, "storage", "sqlite.db")
    c = sqlite3.connect(path)
    c.row_factory = sqlite3.Row
    return c


# ── CORS helper ───────────────────────────────────────────────────────────────
def _cors(response, origin=None):
    response.headers["Access-Control-Allow-Origin"] = origin or "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = (
        "Content-Type, X-API-Key, Authorization"
    )
    return response


# ── API Key validation ────────────────────────────────────────────────────────
def _validate_api_key():
    """
    Extract and validate the API key from:
      - Header:      X-API-Key: <key>
      - Bearer auth: Authorization: Bearer <key>
      - Query param: ?api_key=<key>

    Enforces restriction_type rules:
      - 'all'    → no additional restriction
      - 'ip'     → caller IP must be in allowed_ips list
      - 'domain' → Origin or Referer host must be in allowed_domains list

    Returns (key_row_dict, error_response_tuple).
    Exactly one of them is None.
    """
    raw = (
        request.headers.get("X-API-Key")
        or _bearer_token()
        or request.args.get("api_key", "")
        or (request.get_json(silent=True) or {}).get("api_key", "")
    ).strip()

    if not raw:
        return None, (jsonify(error="API key required",
                              hint="Pass your key via X-API-Key header, "
                                   "Authorization: Bearer <key>, or ?api_key= query param"),
                      401)

    import hashlib as _hashlib
    key_hash = _hashlib.sha256(raw.encode()).hexdigest()

    with _db() as c:
        # Try hash-based lookup first (new keys), fall back to legacy plaintext
        row = c.execute(
            """SELECT k.*, u.plan_id, u.subscription_status, u.is_active AS user_active
               FROM user_api_keys k
               JOIN users u ON u.id = k.user_id
               WHERE k.key_hash = ? AND k.is_active = 1""",
            (key_hash,)
        ).fetchone()
        if not row:
            row = c.execute(
                """SELECT k.*, u.plan_id, u.subscription_status, u.is_active AS user_active
                   FROM user_api_keys k
                   JOIN users u ON u.id = k.user_id
                   WHERE k.api_key = ? AND k.is_active = 1 AND k.key_hash = ''""",
                (raw,)
            ).fetchone()

    if not row:
        return None, (jsonify(error="Invalid or revoked API key"), 401)

    row = dict(row)

    if not row.get("user_active", 1):
        return None, (jsonify(error="Account suspended"), 403)

    rtype = row.get("restriction_type", "all")

    # ── IP restriction ────────────────────────────────────────────────────────
    if rtype == "ip":
        allowed_ips = json.loads(row.get("allowed_ips") or "[]")
        if allowed_ips:
            caller_ip = _get_client_ip()
            if caller_ip not in allowed_ips:
                return None, (jsonify(
                    error="IP not allowed",
                    message=f"This key is restricted to specific IPs. "
                            f"Your IP ({caller_ip}) is not on the allowlist."
                ), 403)

    # ── Domain restriction ────────────────────────────────────────────────────
    elif rtype == "domain":
        allowed_domains = json.loads(row.get("allowed_domains") or "[]")
        if allowed_domains:
            caller_host = _origin_host()
            if not caller_host or not any(
                caller_host == d.lower().lstrip("*. ") or
                caller_host.endswith("." + d.lower().lstrip("*. "))
                for d in allowed_domains
            ):
                return None, (jsonify(
                    error="Domain not allowed",
                    message=f"This key is restricted to specific domains. "
                            f"Request origin ({caller_host or 'unknown'}) is not on the allowlist."
                ), 403)

    # ── Fetch plan limits + check API access ─────────────────────────────────
    plan_daily_limit = 100
    plan_minute_limit = 10
    try:
        with _db() as c:
            plan_row = c.execute(
                "SELECT api_enabled, api_calls_per_day, api_calls_per_minute, name "
                "FROM plans WHERE id=?",
                (row.get("plan_id"),)
            ).fetchone()
        if plan_row:
            if not plan_row["api_enabled"]:
                plan_name = plan_row["name"] or "your current"
                return None, (jsonify(
                    error="API access not available",
                    message=f"The '{plan_name}' plan does not include API access. "
                            f"Please upgrade your subscription to use the API.",
                    plan=plan_name,
                ), 403)
            plan_daily_limit  = plan_row["api_calls_per_day"]
            plan_minute_limit = plan_row["api_calls_per_minute"]
    except Exception:
        pass

    # ── Enforce daily limit ───────────────────────────────────────────────────
    if plan_daily_limit != -1 and plan_daily_limit > 0:
        try:
            with _db() as c:
                used_today = c.execute(
                    "SELECT COUNT(*) FROM api_usage_log "
                    "WHERE user_id=? AND date(created_at)=date('now')",
                    (row["user_id"],)
                ).fetchone()[0]
            if used_today >= plan_daily_limit:
                return None, (jsonify(
                    error="Daily API limit reached",
                    limit=plan_daily_limit,
                    used=used_today,
                    message=f"Your plan allows {plan_daily_limit} API calls per day. "
                            f"Upgrade your subscription for a higher limit.",
                    reset="Limit resets at midnight UTC",
                ), 429)
        except Exception:
            pass

    # ── Enforce per-minute limit ──────────────────────────────────────────────
    if plan_minute_limit != -1 and plan_minute_limit > 0:
        try:
            with _db() as c:
                used_this_minute = c.execute(
                    "SELECT COUNT(*) FROM api_usage_log "
                    "WHERE api_key_id=? AND created_at >= datetime('now', '-1 minute')",
                    (row["id"],)
                ).fetchone()[0]
            if used_this_minute >= plan_minute_limit:
                return None, (jsonify(
                    error="Per-minute API rate limit reached",
                    limit=plan_minute_limit,
                    used=used_this_minute,
                    message=f"Your plan allows {plan_minute_limit} API calls per minute. "
                            f"Please slow down and retry after a moment.",
                ), 429)
        except Exception:
            pass

    # ── Log this call and update usage stats ──────────────────────────────────
    endpoint = request.path or ""
    try:
        with _db() as c:
            c.execute(
                "INSERT INTO api_usage_log (user_id, api_key_id, endpoint) VALUES (?,?,?)",
                (row["user_id"], row["id"], endpoint)
            )
            c.execute(
                "UPDATE user_api_keys SET usage_count = usage_count + 1, "
                "last_used_at = datetime('now') WHERE id = ?",
                (row["id"],)
            )
            c.commit()
    except Exception:
        pass

    row["plan_api_calls_per_day"]    = plan_daily_limit
    row["plan_api_calls_per_minute"] = plan_minute_limit

    return row, None


def _bearer_token():
    auth = request.headers.get("Authorization", "")
    if auth.lower().startswith("bearer "):
        return auth[7:].strip()
    return ""


def _get_client_ip():
    for hdr in ("X-Forwarded-For", "X-Real-IP"):
        val = request.headers.get(hdr)
        if val:
            return val.split(",")[0].strip()
    return request.remote_addr or ""


def _origin_host():
    origin = request.headers.get("Origin") or request.headers.get("Referer") or ""
    try:
        from urllib.parse import urlparse
        return urlparse(origin).hostname or ""
    except Exception:
        return ""


# ── Register routes ───────────────────────────────────────────────────────────
def register_api_routes(app):
    from local_jobs import LocalJob, _local_queue
    import sqlite3 as _sql

    _upload_dir = os.path.join(dir_path, "storage", "uploads")

    # ── Redirect /api/v1/... → api.<domain>/v1/... when subdomains are on ─────
    @app.before_request
    def _api_subdomain_redirect():
        """When subdomain routing is enabled, the canonical API base is
        api.<SERVER_NAME>/v1/ — redirect main-domain /api/v1/... there."""
        if os.environ.get("REPLIT_DEV_DOMAIN"):
            return
        if not request.path.startswith("/api/v1"):
            return
        try:
            from helpers import get_site_settings
            if get_site_settings().get("subdomains_enabled", "0") != "1":
                return
        except Exception:
            return
        server_name = app.config.get("SERVER_NAME")
        if not server_name:
            return
        # Strip the leading /api prefix: /api/v1/convert → /v1/convert
        new_path = request.path[len("/api"):]
        qs = ("?" + request.query_string.decode()) if request.query_string else ""
        code = 308 if request.method not in ("GET", "HEAD") else 301
        return redirect(f"https://api.{server_name}{new_path}{qs}", code=code)

    # ── CORS preflight for every API route ───────────────────────────────────
    @app.before_request
    def _api_cors_preflight():
        if request.method == "OPTIONS" and _is_api_request():
            resp = current_app.make_default_options_response()
            return _cors(resp, request.headers.get("Origin"))

    @app.after_request
    def _api_cors_headers(response):
        if _is_api_request():
            _cors(response, request.headers.get("Origin"))
        return response

    def _is_api_request():
        host = request.host.split(":")[0].lower()
        server_name = (app.config.get("SERVER_NAME") or "").lower()
        if server_name and host == "api." + server_name:
            return True
        if request.path.startswith("/api/v1"):
            return True
        return False

    # ─────────────────────────────────────────────────────────────────────────
    # Route helper: dual-register on subdomain AND main-domain /api/v1 path
    # so it works in Replit dev mode (no SERVER_NAME) too.
    # ─────────────────────────────────────────────────────────────────────────

    def api_route(path, **kwargs):
        """Decorator factory that registers a route both on the api subdomain
        and as /api/v1<path> on the main domain.

        Subdomain routes are registered at both /v1<path> (canonical) and
        <path> (legacy) so that api.domain/v1/formats and api.domain/formats
        both work.
        """
        def decorator(fn):
            # Main domain fallback path: /api/v1/formats etc.
            main_path = "/api/v1" + path
            app.add_url_rule(main_path, endpoint="main_" + fn.__name__,
                             view_func=fn, **kwargs)
            # Subdomain routes (only when SERVER_NAME is configured)
            if not os.environ.get("REPLIT_DEV_DOMAIN"):
                # Canonical: api.domain/v1/formats
                v1_path = "/v1" + path
                try:
                    app.add_url_rule(v1_path, endpoint="apiv1_" + fn.__name__,
                                     view_func=fn, subdomain="api", **kwargs)
                except Exception:
                    pass
                # Legacy / bare: api.domain/formats (keeps existing integrations working)
                try:
                    app.add_url_rule(path, endpoint="api_" + fn.__name__,
                                     view_func=fn, subdomain="api", **kwargs)
                except Exception:
                    pass
            return fn
        return decorator

    # ── GET / (or /v1/) ───────────────────────────────────────────────────────
    @api_route("/")
    def api_root():
        server_name = app.config.get("SERVER_NAME") or request.host.split(":")[0]
        if os.environ.get("REPLIT_DEV_DOMAIN"):
            base_url = f"https://{request.host}/api/v1"
            docs_url = f"https://{request.host}/developers/"
        else:
            base_url = f"https://api.{server_name}/v1"
            docs_url = f"https://developers.{server_name}/"
        return jsonify({
            "name": "OnlineConvert API",
            "version": "v1",
            "docs": docs_url,
            "base_url": base_url,
            "endpoints": {
                "formats":  "GET  /v1/formats",
                "upload":   "POST /v1/upload",
                "convert":  "POST /v1/convert",
                "status":   "GET  /v1/status/<job_id>",
                "download": "GET  /v1/download/<job_id>",
                "key_info": "GET  /v1/keys/me",
            },
            "authentication": "Pass your API key via X-API-Key header, Authorization: Bearer token, or ?api_key= query param",
        })

    # ── GET /formats ──────────────────────────────────────────────────────────
    @api_route("/formats")
    def api_formats():
        key_row, err = _validate_api_key()
        if err:
            return err
        try:
            from configs.filetypes import available_filetypes
            result = {}
            for ft, cfg in available_filetypes.items():
                result[ft] = {
                    "input_formats": cfg.get("allowed", []),
                    "output_formats": cfg.get("ext", []),
                }
            return jsonify(ok=True, filetypes=result)
        except Exception as e:
            return jsonify(ok=False, error=str(e)), 500

    # ── POST /upload ──────────────────────────────────────────────────────────
    @api_route("/upload", methods=["POST", "OPTIONS"])
    def api_upload():
        if request.method == "OPTIONS":
            return _cors(current_app.make_default_options_response(),
                         request.headers.get("Origin"))
        key_row, err = _validate_api_key()
        if err:
            return err

        files = request.files.getlist("file") or request.files.getlist("files")
        if not files or not files[0].filename:
            return jsonify(ok=False, error="No file provided. "
                           "Send your file as multipart/form-data with key 'file'."), 400

        os.makedirs(_upload_dir, exist_ok=True)
        uploaded = []
        for f in files:
            filename = secure_filename(f.filename)
            folder = uuid.uuid4().hex
            folder_path = os.path.join(_upload_dir, folder)
            os.makedirs(folder_path, exist_ok=True)
            save_path = os.path.join(folder_path, filename)
            f.save(save_path)
            file_size = os.path.getsize(save_path)
            rel_path = f"{folder}/{filename}"
            ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
            try:
                with _db() as c:
                    cur = c.execute(
                        "INSERT INTO file_uploads (user_id, session_id, file_path, file_name, "
                        "file_size, original_format) VALUES (?,?,?,?,?,?)",
                        (key_row["user_id"], "", rel_path, filename, file_size, ext)
                    )
                    upload_id = cur.lastrowid
                    c.commit()
            except Exception:
                upload_id = None
            uploaded.append({
                "file_path": rel_path,
                "file_name": filename,
                "file_size": file_size,
                "upload_id": upload_id,
                "extension": ext,
            })

        return jsonify(ok=True, files=uploaded)

    # ── POST /convert ─────────────────────────────────────────────────────────
    @api_route("/convert", methods=["POST", "OPTIONS"])
    def api_convert():
        """
        Upload a file and start a conversion job in one request.

        Form fields (multipart/form-data):
          file          – the file to convert (required)
          filetype      – converter category: image | audio | video | document | pdf | ... (required)
          output_format – target format, e.g. jpg, mp3, pdf (required)
          options       – JSON string of converter options (optional)

        Returns:
          { ok, job_id, status_url, download_url }
        """
        if request.method == "OPTIONS":
            return _cors(current_app.make_default_options_response(),
                         request.headers.get("Origin"))
        key_row, err = _validate_api_key()
        if err:
            return err

        data = request.form or (request.get_json(silent=True) or {})
        filetype = (data.get("filetype") or "").strip().lower()
        output_format = (data.get("output_format") or data.get("format") or "").strip().lower()

        if not filetype or filetype not in API_ALLOWED_FILETYPES:
            return jsonify(ok=False,
                           error=f"Invalid or missing 'filetype'. "
                                 f"Allowed: {', '.join(API_ALLOWED_FILETYPES)}"), 400
        if not output_format:
            return jsonify(ok=False, error="Missing 'output_format'"), 400

        # Accept file from upload or from previously uploaded path
        file_path_rel = None
        upload_id = None

        file = (request.files.get("file") or
                (request.files.getlist("files") or [None])[0])

        if file and file.filename:
            filename = secure_filename(file.filename)
            folder = uuid.uuid4().hex
            folder_path = os.path.join(_upload_dir, folder)
            os.makedirs(folder_path, exist_ok=True)
            save_path = os.path.join(folder_path, filename)
            file.save(save_path)
            file_size = os.path.getsize(save_path)
            file_path_rel = f"{folder}/{filename}"
            ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
            try:
                with _db() as c:
                    cur = c.execute(
                        "INSERT INTO file_uploads (user_id, session_id, file_path, file_name, "
                        "file_size, original_format) VALUES (?,?,?,?,?,?)",
                        (key_row["user_id"], "", file_path_rel, filename, file_size, ext)
                    )
                    upload_id = cur.lastrowid
                    c.commit()
            except Exception:
                upload_id = None
        else:
            file_path_rel = (data.get("file_path") or "").strip()
            upload_id = data.get("upload_id")
            if not file_path_rel:
                return jsonify(ok=False,
                               error="No file provided. Send 'file' as multipart/form-data "
                                     "or set 'file_path' from a prior /upload call."), 400

        # Options
        try:
            options_dict = json.loads(data.get("options") or "{}")
        except Exception:
            options_dict = {}

        # Build file URL list for converter
        full_path = os.path.join(_upload_dir, file_path_rel)
        file_urls = [json.dumps({"path": file_path_rel, "upload_id": upload_id})]

        # Enqueue conversion job
        try:
            from converters import (
                image_converter, audio_converter, video_converter,
                document_converter, hash_generator,
            )
            _conv_map = {
                "image":    image_converter,
                "audio":    audio_converter,
                "video":    video_converter,
                "document": document_converter,
                "hash":     hash_generator,
            }
        except Exception:
            _conv_map = {}

        if filetype not in _conv_map:
            # Try dynamic import
            try:
                import importlib
                mod = importlib.import_module(f"converters.{filetype.replace('-', '_')}_converter")
                _conv_map[filetype] = mod
            except Exception:
                pass

        if filetype not in _conv_map:
            return jsonify(ok=False,
                           error=f"Converter for '{filetype}' not available via API yet."), 400

        conv_args = (file_urls, output_format, options_dict, {
            "UPLOAD_DIR": _upload_dir,
            "BUCKET": app.config.get("BUCKET", ""),
            "LOCAL": app.config.get("LOCAL", True),
        })

        job = _local_queue.enqueue_call(
            func=_conv_map[filetype].convert,
            args=conv_args,
            result_ttl=5000,
            timeout=300,
            queue_name="low",
        )

        job_id = job.id if hasattr(job, "id") else str(job)
        input_ext = file_path_rel.rsplit(".", 1)[-1].lower() if "." in file_path_rel else filetype

        try:
            with _db() as c:
                c.execute(
                    "INSERT INTO conversions (user_id, upload_id, input_format, output_format, "
                    "job_id, status, file_name, ip_address, tool_type) VALUES (?,?,?,?,?,?,?,?,?)",
                    (key_row["user_id"], upload_id, input_ext, output_format,
                     job_id, "pending",
                     os.path.basename(file_path_rel),
                     _get_client_ip(), "converter")
                )
                c.commit()
        except Exception:
            pass

        server_name = app.config.get("SERVER_NAME") or request.host.split(":")[0]
        base = f"https://api.{server_name}/v1"
        if os.environ.get("REPLIT_DEV_DOMAIN"):
            base = f"https://{request.host}/api/v1"

        return jsonify(
            ok=True,
            job_id=job_id,
            status="pending",
            status_url=f"{base}/status/{job_id}",
            download_url=f"{base}/download/{job_id}",
        ), 202

    # ── GET /status/<job_id> ──────────────────────────────────────────────────
    @api_route("/status/<job_id>")
    def api_status(job_id):
        key_row, err = _validate_api_key()
        if err:
            return err

        # Check DB first
        db_status = None
        db_output = None
        try:
            with _db() as c:
                row = c.execute(
                    "SELECT status, output_format, output_path, error_message, "
                    "created_at, completed_at FROM conversions WHERE job_id=? "
                    "ORDER BY id DESC LIMIT 1",
                    (job_id,)
                ).fetchone()
            if row:
                db_status = row["status"]
                db_output = row["output_path"]
        except Exception:
            pass

        # Check LocalJob
        try:
            job = LocalJob.fetch(job_id)
            if job.is_finished:
                status = "finished"
            elif job.is_failed:
                status = "failed"
            elif job.is_started:
                status = "processing"
            else:
                status = "pending"
        except Exception:
            status = db_status or "unknown"

        server_name = app.config.get("SERVER_NAME") or request.host.split(":")[0]
        base = f"https://api.{server_name}/v1"
        if os.environ.get("REPLIT_DEV_DOMAIN"):
            base = f"https://{request.host}/api/v1"

        result = dict(ok=True, job_id=job_id, status=status)
        if status == "finished":
            result["download_url"] = f"{base}/download/{job_id}"
        if status == "failed":
            try:
                result["error"] = (row["error_message"] if row else "") or "Conversion failed"
            except Exception:
                result["error"] = "Conversion failed"

        return jsonify(result)

    # ── GET /download/<job_id> ────────────────────────────────────────────────
    @api_route("/download/<job_id>")
    def api_download(job_id):
        key_row, err = _validate_api_key()
        if err:
            return err

        # Look up job result
        try:
            job = LocalJob.fetch(job_id)
        except Exception:
            job = None

        output_path = None

        if job and job.is_finished and job.result:
            r = job.result
            if isinstance(r, dict):
                results = r.get("results") or []
                output_path = (r.get("output_path") or
                               (results[0] if results else "") or
                               r.get("path") or "")
            elif isinstance(r, str):
                output_path = r

        if not output_path:
            # Fallback to DB
            try:
                with _db() as c:
                    row = c.execute(
                        "SELECT output_path, status FROM conversions "
                        "WHERE job_id=? ORDER BY id DESC LIMIT 1",
                        (job_id,)
                    ).fetchone()
                if row:
                    if row["status"] != "finished":
                        return jsonify(ok=False,
                                       error=f"Job is not finished yet (status: {row['status']})"), 409
                    output_path = row["output_path"]
            except Exception:
                pass

        if not output_path:
            return jsonify(ok=False, error="Job not found or result not available"), 404

        # Resolve to absolute path
        if not os.path.isabs(output_path):
            full_path = os.path.join(_upload_dir, output_path)
        else:
            full_path = output_path

        if not os.path.isfile(full_path):
            return jsonify(ok=False, error="Output file not found on disk"), 404

        return send_file(
            full_path,
            as_attachment=True,
            download_name=os.path.basename(full_path),
        )

    # ── GET /keys/me — inspect current key + rate-limit status ───────────────
    @api_route("/keys/me")
    def api_key_info():
        key_row, err = _validate_api_key()
        if err:
            return err

        daily_limit  = key_row.get("plan_api_calls_per_day", -1)
        minute_limit = key_row.get("plan_api_calls_per_minute", -1)

        used_today = 0
        used_this_minute = 0
        try:
            with _db() as c:
                used_today = c.execute(
                    "SELECT COUNT(*) FROM api_usage_log "
                    "WHERE user_id=? AND date(created_at)=date('now')",
                    (key_row["user_id"],)
                ).fetchone()[0]
                used_this_minute = c.execute(
                    "SELECT COUNT(*) FROM api_usage_log "
                    "WHERE api_key_id=? AND created_at >= datetime('now', '-1 minute')",
                    (key_row["id"],)
                ).fetchone()[0]
        except Exception:
            pass

        return jsonify(
            ok=True,
            key_id=key_row["id"],
            name=key_row["name"],
            restriction_type=key_row["restriction_type"],
            allowed_ips=json.loads(key_row.get("allowed_ips") or "[]"),
            allowed_domains=json.loads(key_row.get("allowed_domains") or "[]"),
            usage_count=key_row["usage_count"],
            last_used_at=key_row["last_used_at"],
            created_at=key_row["created_at"],
            rate_limits={
                "calls_per_day":    daily_limit,
                "calls_per_minute": minute_limit,
                "used_today":       used_today,
                "used_this_minute": used_this_minute,
                "remaining_today":  (daily_limit - used_today) if daily_limit != -1 else -1,
            },
        )
