#!/usr/bin/env python3
"""
Screaming Frog licences from Snipe-IT → data/screaming-frog.json for the IT dashboard.

Reads software licences in Snipe-IT (manufacturer Screaming Frog) via the same API credentials
as app/src/jobs/fetch_snipe_it.py. Active = not deleted and not past expiration_date.

Env (same Snipe-IT app registration as fetch_snipe_it.py):
  SNIPE_URL, SNIPE_API_KEY
  SCREAMING_FROG_PORTAL_URL          — default https://www.screamingfrog.co.uk/my-account/
  SCREAMING_FROG_SEO_SPIDER_VERSION  — e.g. 24.1 (dashboard tile)
  SCREAMING_FROG_INCLUDE_EXPIRED     — set 1 to include expired Snipe-IT licence rows
  SCREAMING_FROG_EXPIRY_ALERT_DAYS   — dashboard Alerts when expiry is within N days (default 30)
  SCREAMING_FROG_EXPIRY_ALERT_MAX    — max expiring rows in expiringAlertDetail (default 50; max 500)
  SCREAMING_FROG_OUT                 — default data/screaming-frog.json (or MariaDB feed snapshot)

Setup:
  1. cp env.example .env — set SNIPE_URL and SNIPE_API_KEY (already used for Snipe-IT section).
  2. python3 app/src/jobs/fetch_screaming_frog.py
"""
from __future__ import annotations

import json
import os
import sys
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any

import requests

from store import write_json_snapshot

from env_config import env_str
from paths import DATA_DIR, REPO_ROOT, load_env_file

ROOT = REPO_ROOT
OUT = Path(os.environ.get("SCREAMING_FROG_OUT", str(DATA_DIR / "screaming-frog.json")))


def load_env_file() -> None:
    path = REPO_ROOT / ".env"
    if not path.is_file():
        return
    for raw in path.read_text(encoding="utf-8-sig").splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        if line.startswith("export "):
            line = line[7:].strip()
        if "=" not in line:
            continue
        key, _, val = line.partition("=")
        key = key.strip()
        val = val.strip().strip('"').strip("'")
        if not key:
            continue
        if val:
            os.environ[key] = val
            continue
        prev = os.environ.get(key)
        if prev is None or not str(prev).strip():
            os.environ[key] = val


def instance_web_url(api_base: str) -> str:
    base = api_base.rstrip("/")
    for suffix in ("/api/v1", "/api"):
        if base.lower().endswith(suffix):
            return base[: -len(suffix)] or base
    return base


def _license_rows(data: Any) -> list[dict[str, Any]]:
    if not isinstance(data, dict):
        return []
    rows = data.get("rows")
    if isinstance(rows, list):
        return [r for r in rows if isinstance(r, dict)]
    return []


def _expiration_iso(lic: dict[str, Any]) -> str:
    exp = lic.get("expiration_date")
    if isinstance(exp, dict):
        return str(exp.get("date") or "").strip()
    return ""


def _product_label(name: str) -> str:
    low = name.lower()
    if "seo spider" in low:
        return "SEO Spider"
    if "log file" in low:
        return "Log File Analyser"
    return name.strip() or "Screaming Frog"


def _is_screaming_frog_license(lic: dict[str, Any]) -> bool:
    match = env_str("SCREAMING_FROG_SNIPE_MATCH").lower()
    mfr = lic.get("manufacturer")
    if isinstance(mfr, dict) and match in str(mfr.get("name") or "").lower():
        return True
    return match in str(lic.get("name") or "").lower()


def _license_is_active(lic: dict[str, Any], today: str, include_expired: bool) -> bool:
    if lic.get("deleted_at"):
        return False
    if include_expired:
        return True
    exp = _expiration_iso(lic)
    return not exp or exp >= today


def _seat_count(lic: dict[str, Any]) -> int:
    try:
        seats = int(lic.get("seats") or 1)
    except (TypeError, ValueError):
        seats = 1
    return max(seats, 1)


def _free_seats(lic: dict[str, Any]) -> int:
    try:
        return max(int(lic.get("free_seats_count") or 0), 0)
    except (TypeError, ValueError):
        return 0


def fetch_screaming_frog_licenses(
    session: requests.Session, base: str, include_expired: bool
) -> tuple[list[dict[str, Any]], str | None]:
    today = date.today().isoformat()
    limit = 500
    offset = 0
    matched: list[dict[str, Any]] = []
    try:
        while True:
            r = session.get(
                f"{base}/licenses",
                params={"limit": limit, "offset": offset},
                timeout=120,
            )
            if not r.ok:
                safe = r.url.split("?", 1)[0] if getattr(r, "url", None) else f"{base}/licenses"
                return [], (
                    f"Snipe-IT GET {safe} → HTTP {r.status_code}. "
                    "Check SNIPE_URL and SNIPE_API_KEY in .env."
                )
            data = r.json()
            rows = _license_rows(data)
            for lic in rows:
                if not _is_screaming_frog_license(lic):
                    continue
                if not _license_is_active(lic, today, include_expired):
                    continue
                matched.append(lic)
            if len(rows) < limit:
                break
            offset += len(rows)
    except requests.RequestException as ex:
        return [], f"Snipe-IT request failed: {ex}"

    matched.sort(key=lambda lic: (_product_label(str(lic.get("name") or "")), str(lic.get("license_email") or "").lower()))
    return matched, None


def _parse_positive_int(name: str, default: int, *, max_value: int | None = None) -> int:
    raw = os.environ.get(name, "").strip()
    if not raw:
        return default
    try:
        val = int(raw)
    except ValueError:
        return default
    if val < 0:
        return default
    if max_value is not None:
        return min(val, max_value)
    return val


def _days_until_expiry(expires: str) -> int | None:
    if not expires:
        return None
    try:
        exp = date.fromisoformat(str(expires).strip()[:10])
    except ValueError:
        return None
    return (exp - date.today()).days


def _export_license_row(lic: dict[str, Any]) -> dict[str, Any]:
    name = str(lic.get("name") or "").strip()
    seats = _seat_count(lic)
    free = _free_seats(lic)
    assigned = free < seats
    email = str(lic.get("license_email") or "").strip()
    username = str(lic.get("license_name") or "").strip()
    return {
        "snipeLicenseId": lic.get("id"),
        "name": name,
        "product": _product_label(name),
        "username": username,
        "email": email,
        "expires": _expiration_iso(lic),
        "seats": seats,
        "freeSeats": free,
        "status": "Assigned" if assigned else "Available",
        "productKey": str(lic.get("product_key") or "").strip(),
        "notes": str(lic.get("notes") or "").strip(),
    }


def main() -> int:
    load_env_file()
    base = os.environ.get("SNIPE_URL", "").strip().rstrip("/")
    token = os.environ.get("SNIPE_API_KEY", "").strip()
    portal_url = env_str("SCREAMING_FROG_PORTAL_URL")
    version = env_str("SCREAMING_FROG_SEO_SPIDER_VERSION")
    version_display = version if version.lower().startswith("v") else f"v{version}"
    include_expired = os.environ.get("SCREAMING_FROG_INCLUDE_EXPIRED", "").strip().lower() in (
        "1",
        "true",
        "yes",
    )

    if not base or not token:
        payload = {
            "updatedAt": datetime.now(timezone.utc).isoformat(),
            "portalUrl": portal_url,
            "seoSpiderVersion": version_display,
            "source": "snipe-it",
            "licencesTotal": None,
            "licencesAssigned": None,
            "licencesAvailable": None,
            "seoSpiderLicences": None,
            "logAnalyserLicences": None,
            "licenses": [],
            "assignees": [],
            "screamingFrogError": "Missing SNIPE_URL or SNIPE_API_KEY in .env (see env.example).",
        }
        out_path = write_json_snapshot(OUT, payload)
        print(payload["screamingFrogError"], file=sys.stderr)
        return 1

    session = requests.Session()
    session.headers.update(
        {"Authorization": f"Bearer {token}", "Accept": "application/json"}
    )

    raw_licenses, err = fetch_screaming_frog_licenses(session, base, include_expired)
    expiry_alert_days = _parse_positive_int("SCREAMING_FROG_EXPIRY_ALERT_DAYS", 30)
    expiry_alert_max = _parse_positive_int("SCREAMING_FROG_EXPIRY_ALERT_MAX", 50, max_value=500)

    licenses = []
    for lic in raw_licenses:
        row = _export_license_row(lic)
        days = _days_until_expiry(str(row.get("expires") or ""))
        if days is not None:
            row["daysUntilExpiry"] = days
        licenses.append(row)

    expiring_all = [
        row
        for row in licenses
        if row.get("daysUntilExpiry") is not None
        and int(row["daysUntilExpiry"]) <= expiry_alert_days
    ]
    expiring_all.sort(key=lambda row: (int(row.get("daysUntilExpiry") or 0), str(row.get("email") or "").lower()))

    assignees = [
        {
            "username": row["username"],
            "email": row["email"],
            "product": row["product"],
            "expires": row["expires"],
            "status": row["status"],
            "snipeLicenseId": row["snipeLicenseId"],
            "notes": row["notes"],
        }
        for row in licenses
        if row.get("email")
    ]

    total_seats = sum(row["seats"] for row in licenses)
    assigned_seats = sum(row["seats"] - row["freeSeats"] for row in licenses)
    available_seats = max(total_seats - assigned_seats, 0)
    seo_count = sum(1 for row in licenses if row["product"] == "SEO Spider")
    lfa_count = sum(1 for row in licenses if row["product"] == "Log File Analyser")

    payload: dict[str, Any] = {
        "updatedAt": datetime.now(timezone.utc).isoformat(),
        "portalUrl": portal_url,
        "seoSpiderVersion": version_display,
        "source": "snipe-it",
        "snipeInstanceUrl": instance_web_url(base),
        "licencesTotal": total_seats,
        "licencesAssigned": assigned_seats,
        "licencesAvailable": available_seats,
        "seoSpiderLicences": seo_count,
        "logAnalyserLicences": lfa_count,
        "licenses": licenses,
        "assignees": assignees,
        "expiryAlertDays": expiry_alert_days,
        "expiringAlertTotal": len(expiring_all),
        "expiringAlertTruncated": len(expiring_all) > expiry_alert_max,
        "expiringAlertDetail": expiring_all[:expiry_alert_max],
        "screamingFrogError": err,
    }

    out_path = write_json_snapshot(OUT, payload)
    print(
        f"Wrote {out_path} ({assigned_seats} assigned / {total_seats} active seats from Snipe-IT)"
    )
    if err:
        print(f"Note: {err}", file=sys.stderr)
    return 0 if not err else 1


if __name__ == "__main__":
    raise SystemExit(main())
