import json
import os
import time
from pathlib import Path
from typing import Optional, Tuple

import requests


def _load_env_next_to_script() -> None:
    env_path = Path(__file__).resolve().parent / ".env"
    if not env_path.is_file():
        return
    try:
        for raw in env_path.read_text(encoding="utf-8").splitlines():
            line = raw.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, _, val = line.partition("=")
            key, val = key.strip(), val.strip().strip("'\"")
            if key:
                os.environ[key] = val
    except OSError:
        pass


_load_env_next_to_script()

# --- CONFIGURATION VIA ENVIRONMENT ---
# No hardcoded paths: values come from .env or sensible defaults.
API_URL = os.environ.get("API_URL", "https://loglytics.tech/api/logs")
SERVER_ID = os.environ.get("SERVER_ID", "")
API_KEY = os.environ.get("API_KEY", "")

# Mutable: synced from dashboard via GET /api/servers/{id}/settings
current_log_file = os.environ.get("LOG_FILE", "/var/log/syslog")

# Stateful read pointer (same directory as this script)
_AGENT_DIR = Path(__file__).resolve().parent
POSITION_FILE = _AGENT_DIR / ".log_position"

MAX_LINES_PER_SEND = 100


def build_settings_url() -> str:
    """Derive settings URL from API_URL (same origin as POST /api/logs)."""
    u = API_URL.strip().rstrip("/")
    marker = "/api/logs"
    if u.endswith(marker):
        return f"{u[: -len(marker)]}/api/servers/{SERVER_ID}/settings"
    return f"{u}/api/servers/{SERVER_ID}/settings"


def fetch_remote_log_path() -> Optional[str]:
    """Return canonical log_path from dashboard, or None on failure."""
    if not SERVER_ID or not API_KEY:
        return None
    url = build_settings_url()
    try:
        response = requests.get(
            url,
            headers={"x-api-key": API_KEY},
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()
        if not isinstance(data, dict):
            return None
        raw = data.get("log_path")
        if isinstance(raw, str) and raw.strip():
            return raw.strip()
    except (requests.RequestException, json.JSONDecodeError, ValueError) as e:
        print(f"⚠️ Settings sync failed: {e}")
    return None


def load_saved_position() -> int:
    """Last successful read byte offset; 0 if missing or invalid."""
    if not POSITION_FILE.is_file():
        return 0
    try:
        raw = POSITION_FILE.read_text(encoding="utf-8").strip()
        return max(0, int(raw))
    except (OSError, ValueError):
        return 0


def save_position(byte_offset: int) -> None:
    try:
        POSITION_FILE.write_text(str(byte_offset), encoding="utf-8")
    except OSError as e:
        print(f"⚠️ Could not save .log_position: {e}")


def collect_new_log_text(log_path: str, start_byte: int) -> Tuple[Optional[str], int]:
    """
    Read new bytes from log_path starting at start_byte.

    Returns (text_to_send_or_None, end_byte_after_read).
    - None means: nothing to send (no new complete lines to process this tick).
    - On rotation (file shrank), start from 0.
    - If more than MAX_LINES_PER_SEND lines, only the last MAX_LINES_PER_SEND are kept;
      on success the caller should still save end_byte to EOF so skipped bytes are not resent.
    """
    if not os.path.exists(log_path):
        print(f"⚠️ File not found: {log_path}")
        return None, start_byte

    try:
        size = os.path.getsize(log_path)
    except OSError as e:
        print(f"⚠️ Could not stat log file: {e}")
        return None, start_byte

    pos = start_byte
    if size < pos:
        # Log rotated / truncated: start from beginning
        pos = 0
        if size == 0:
            # Nothing to read; persist 0 so we do not loop with a stale offset
            save_position(0)
            return None, 0

    try:
        with open(log_path, "rb") as f:
            f.seek(pos)
            new_bytes = f.read()
    except OSError as e:
        print(f"⚠️ Could not read log file: {e}")
        return None, start_byte

    end_byte = size

    if not new_bytes:
        return None, start_byte

    text = new_bytes.decode("utf-8", errors="replace")
    lines = text.splitlines()
    if len(lines) < 1:
        return None, start_byte

    if len(lines) > MAX_LINES_PER_SEND:
        lines = lines[-MAX_LINES_PER_SEND:]

    payload = "\n".join(lines)
    if text.endswith("\n") and not payload.endswith("\n"):
        payload += "\n"

    return payload, end_byte


def main():
    global current_log_file

    print(f"🚀 Loglytics agent started. Watching: {current_log_file}")

    # Skip API calls when the payload is identical to the last successful send
    last_sent_payload: Optional[str] = None

    try:
        while True:
            remote = fetch_remote_log_path()
            if remote is not None and remote != current_log_file:
                current_log_file = remote
                os.environ["LOG_FILE"] = current_log_file
                save_position(0)
                last_sent_payload = None
                print(f"Syncing: Now watching [{current_log_file}]")

            start = load_saved_position()
            log_text, end_byte = collect_new_log_text(current_log_file, start)

            if log_text is not None and log_text.strip():
                if (
                    last_sent_payload is not None
                    and log_text == last_sent_payload
                ):
                    time.sleep(2)
                    continue
                try:
                    response = requests.post(
                        API_URL,
                        json={"server_id": SERVER_ID, "log_text": log_text},
                        headers={"x-api-key": API_KEY},
                        timeout=120,
                    )
                    response.raise_for_status()
                except requests.RequestException as e:
                    print(f"⚠️ API request failed (position not updated): {e}")
                else:
                    save_position(end_byte)
                    last_sent_payload = log_text

            time.sleep(2)  # Poll every minute
    except KeyboardInterrupt:
        print("Agent stopped")


if __name__ == "__main__":
    main()
