#!/usr/bin/env python3
import argparse
import gzip
import io
import json
import os
import sys
import tarfile
import time
import urllib.error
import urllib.parse
import urllib.request
from collections import deque
from pathlib import Path

API_URL = os.environ.get("LOGVERA_API_URL", "https://api-logvera.taaranjain.com").rstrip("/")
VERSION = "1.1.0"
COMPRESSED = (".gz", ".tgz", ".zip", ".tar")


def fail(message, code=2):
    print(f"logvera: {message}", file=sys.stderr)
    sys.exit(code)


def call(method, path, key=None, access=None, body=None, headers=None, timeout=300):
    request_headers = {"User-Agent": f"logvera-cli/{VERSION}", "Accept": "application/json"}
    if key:
        request_headers["Authorization"] = f"Bearer {key}"
    if access:
        request_headers["X-Access-Key"] = access
    request_headers.update(headers or {})
    request = urllib.request.Request(API_URL + path, data=body, method=method, headers=request_headers)
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            raw = response.read()
            return json.loads(raw) if raw else {}
    except urllib.error.HTTPError as exc:
        try:
            detail = json.loads(exc.read()).get("error", {}).get("message", exc.reason)
        except ValueError:
            detail = exc.reason
        fail(f"{detail} (HTTP {exc.code})")
    except urllib.error.URLError as exc:
        fail(f"could not reach {API_URL}: {exc.reason}")


def strip_gz(name):
    return name[:-3] if name.lower().endswith(".gz") else name


def read_lines(path):
    opener = gzip.open if path.name.lower().endswith(".gz") and not path.name.lower().endswith(".tar.gz") else open
    return opener(path, "rb")


def build_payload(paths, tail, name):
    if len(paths) == 1:
        path = paths[0]
        lower = path.name.lower()
        if lower.endswith(COMPRESSED) and not tail:
            return path.read_bytes(), path.name, path.name
        if lower.endswith((".zip", ".tar", ".tgz", ".tar.gz")):
            fail("--tail can't be used with archives")
        with read_lines(path) as handle:
            data = b"".join(deque(handle, maxlen=tail)) if tail else handle.read()
        label = name or strip_gz(path.name)
        if ".log" not in label.lower():
            label += ".log"
        return gzip.compress(data, compresslevel=6), label + ".gz", label
    buffer = io.BytesIO()
    with tarfile.open(fileobj=buffer, mode="w:gz", compresslevel=6) as archive:
        for path in paths:
            if path.name.lower().endswith((".zip", ".tar", ".tgz", ".tar.gz")):
                fail(f"{path.name}: send archives on their own, not mixed with other files")
            if tail:
                with read_lines(path) as handle:
                    data = b"".join(deque(handle, maxlen=tail))
                info = tarfile.TarInfo(strip_gz(path.name))
                info.size = len(data)
                info.mtime = int(time.time())
                archive.addfile(info, io.BytesIO(data))
            else:
                archive.add(str(path), arcname=path.name, recursive=False)
    label = name or "+".join(sorted(p.name for p in paths))[:100]
    return buffer.getvalue(), "logs.tar.gz", label


def analyze(args):
    key = args.key or os.environ.get("LOGVERA_API_KEY")
    if not key:
        fail("set LOGVERA_API_KEY or pass --key")
    paths = [Path(f) for f in args.files]
    for path in paths:
        if not path.is_file():
            fail(f"{path} is not a file")
        if path.stat().st_size == 0 and len(paths) == 1:
            fail(f"{path} is empty")
    body, name, label = build_payload(paths, args.tail, args.name)
    size = len(body)
    if size == 0:
        fail("nothing to send")
    params = {
        "sensitivity": args.sensitivity,
        "ai": "false" if args.no_ai else "true",
        "notify": "false" if args.no_notify else "true",
        "stream": args.stream or label,
    }
    created = call(
        "POST",
        "/api/v1/analyses?" + urllib.parse.urlencode(params),
        key=key,
        body=body,
        headers={"Content-Type": "application/octet-stream", "Content-Length": str(size), "X-Filename": name},
    )
    if not args.quiet:
        print(f"Uploaded {label} ({size / 1024 / 1024:.1f} MB compressed). Analysis {created['id']} started.")
    if not args.wait and args.fail_under is None and not args.fail_on_alert:
        print(created.get("url", created["id"]))
        return 0
    deadline = time.time() + args.timeout
    status = {}
    while time.time() < deadline:
        status = call("GET", f"/api/v1/analyses/{created['id']}/status", access=created["access_key"])
        if status["state"] == "failed":
            fail((status.get("error") or {}).get("message", "analysis failed"))
        if (
            status["state"] == "ready"
            and status.get("ai_state") != "pending"
            and status.get("alert_state") != "pending"
        ):
            break
        time.sleep(3)
    else:
        fail("timed out waiting for the analysis")
    report = call("GET", f"/api/v1/analyses/{created['id']}", access=created["access_key"])
    kpis = report["kpis"]
    if args.json:
        print(
            json.dumps(
                {
                    "id": created["id"],
                    "url": created.get("url"),
                    "kpis": kpis,
                    "ai": report.get("ai", {}),
                    "security": report["security"],
                    "recommendations": report["recommendations"],
                    "comparison": report.get("run_comparison"),
                    "notify": status.get("notify"),
                    "archive": report["file"].get("archive"),
                },
                indent=2,
            )
        )
    else:
        headline = (report.get("ai") or {}).get("headline") or kpis["health_label"]
        print("")
        print(f"  {headline}")
        print(f"  Health        {kpis['health_score']}/100 ({kpis['health_label']})")
        print(f"  Error rate    {kpis['error_rate'] * 100:.2f}% ({kpis['errors']} errors)")
        if kpis.get("availability") is not None:
            print(f"  Availability  {kpis['availability'] * 100:.3f}%")
        print(f"  Unusual       {kpis['incidents']} period(s)")
        print(f"  Security      {kpis['security_score']}/100 ({len(report['security']['findings'])} findings)")
        for rec in report.get("recommendations", [])[:3]:
            print(f"  -> {rec['title']} ({rec['priority']})")
        archive = report["file"].get("archive") or {}
        if len(archive.get("parts") or []) > 1:
            print(
                f"  Files         {len(archive['parts'])} merged: " + ", ".join(p["name"] for p in archive["parts"][:5])
            )
        print_comparison(report.get("run_comparison"), status.get("notify"))
        print("")
        print(f"  Open: {created.get('url', created['id'])}")
        print("  The analysis is deleted automatically one hour after upload.")
    if args.fail_under is not None and kpis["health_score"] < args.fail_under:
        return 3
    alert = (report.get("run_comparison") or {}).get("alert") or {}
    if args.fail_on_alert and alert.get("reasons"):
        return 4
    return 0


def print_comparison(comparison, notify):
    if not comparison:
        return
    changes = comparison.get("changes")
    print("")
    if changes:
        when = time.strftime("%d %b %H:%M", time.localtime(changes.get("previous_at") or 0))
        print(f"  Since the previous run ({when})")
        m = changes.get("metrics", {})
        if "health" in m:
            print(f"    Health      {m['health']['before']} -> {m['health']['after']} ({m['health']['delta']:+.0f})")
        if "error_rate" in m:
            e = m["error_rate"]
            print(f"    Error rate  {e['before'] * 100:.2f}% -> {e['after'] * 100:.2f}%")
        if "incidents" in m:
            print(f"    Unusual     {m['incidents']['before']} -> {m['incidents']['after']}")
        for title in changes.get("new_findings", [])[:5]:
            print(f"    New finding {title}")
    else:
        print("  First run for this log. Tomorrow's run will be compared with this one.")
    alert = comparison.get("alert") or {}
    for reason in alert.get("reasons", []):
        print(f"  ! {reason['text']}")
    if notify == {} and alert.get("fired"):
        print("  This key has no Slack, webhook or email destination, so nothing was sent.")
    if notify:
        sent = [k for k, v in notify.items() if v == "sent"]
        if notify.get("skipped"):
            print("  No alert sent: nothing crossed your thresholds.")
        elif sent:
            print(f"  Alert sent to {', '.join(sent)}.")
        failed = {k: v for k, v in notify.items() if k not in ("skipped",) and v not in ("sent", "no_conditions_met")}
        for target, result in failed.items():
            print(f"  Could not notify {target}: {result}")


def whoami(args):
    key = args.key or os.environ.get("LOGVERA_API_KEY")
    if not key:
        fail("set LOGVERA_API_KEY or pass --key")
    info = call("GET", "/api/v1/keys/me", key=key)
    print(json.dumps(info, indent=2))
    return 0


def main():
    parser = argparse.ArgumentParser(prog="logvera", description="Send logs to Logvera for analysis.")
    parser.add_argument("--version", action="version", version=VERSION)
    sub = parser.add_subparsers(dest="command", required=True)
    a = sub.add_parser("analyze", help="upload a log file and optionally wait for the summary")
    a.add_argument("files", nargs="+", metavar="FILE", help="log files (.log, .gz) or one .zip/.tar.gz archive")
    a.add_argument("--key", help="API key (defaults to LOGVERA_API_KEY)")
    a.add_argument("--name", help="file name to show in the report")
    a.add_argument(
        "--stream", help="name used to compare with previous runs (defaults to the file name, digits ignored)"
    )
    a.add_argument("--tail", type=int, metavar="N", help="only send the last N lines")
    a.add_argument("--sensitivity", choices=["low", "balanced", "high"], default="balanced")
    a.add_argument("--no-ai", action="store_true", help="skip AI summaries")
    a.add_argument("--no-notify", action="store_true", help="don't post to Slack, webhook or email")
    a.add_argument("--wait", action="store_true", help="wait and print the summary")
    a.add_argument("--fail-under", type=int, metavar="SCORE", help="exit with code 3 if health is below SCORE")
    a.add_argument("--fail-on-alert", action="store_true", help="exit with code 4 if any alert rule was triggered")
    a.add_argument("--timeout", type=int, default=900, help="seconds to wait (default 900)")
    a.add_argument("--json", action="store_true", help="print the summary as JSON")
    a.add_argument("--quiet", action="store_true")
    a.set_defaults(func=analyze)
    w = sub.add_parser("whoami", help="show details about your API key")
    w.add_argument("--key")
    w.set_defaults(func=whoami)
    args = parser.parse_args()
    sys.exit(args.func(args))


if __name__ == "__main__":
    main()
