# -*- coding: utf-8 -*-
import ftplib
import io
import json
import os
import re
import socket
import uuid
from collections import defaultdict
from datetime import datetime
from urllib.request import urlopen

import xbmc
import xbmcvfs

from resources.lib.utils.log_utils import get_logger

log = get_logger()

UPLOAD_INTERVAL_SEC = 12 * 3600
IPIFY_TIMEOUT_SEC = 2
FTP_TIMEOUT_SEC = 15

INSTALL_ID_FILE = "telemetry_install_id.txt"
START_LOG_FILE = "telemetry_start_log.jsonl"
PENDING_LOG_FILE = "telemetry_pending.jsonl"
LAST_UPLOAD_FILE = "telemetry_last_upload.txt"
MIGRATED_V2_FLAG = "telemetry_migrated_v2.txt"

_KODI_VER_RE = re.compile(r"(\d+\.\d+)")
_MONTH_RE = re.compile(r"^(\d{4}-\d{2})$")
_ALL_USERS_RE = re.compile(r"^all_users-(\d{4}-\d{2})\.jsonl$")
_START_MONTH_RE = re.compile(r"^start-(\d{4}-\d{2})\.jsonl$")


def _profile_dir(addon):
    return xbmcvfs.translatePath(addon.getAddonInfo("profile"))


def _ensure_profile_dir(path):
    try:
        if not xbmcvfs.exists(path):
            xbmcvfs.mkdirs(path)
    except Exception:
        pass


def _path(addon, filename):
    pdir = _profile_dir(addon)
    _ensure_profile_dir(pdir)
    return os.path.join(pdir, filename)


def _read_text(path, default=""):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read().strip()
    except Exception:
        return default


def _write_text(path, value):
    with open(path, "w", encoding="utf-8") as f:
        f.write(value or "")


def _append_jsonl(path, row):
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(row, ensure_ascii=False) + "\n")


def _get_or_create_install_id(addon):
    path = _path(addon, INSTALL_ID_FILE)
    val = _read_text(path, "")
    if val:
        return val
    val = str(uuid.uuid4())
    _write_text(path, val)
    return val


def _get_public_ip():
    try:
        with urlopen("https://api.ipify.org?format=json", timeout=IPIFY_TIMEOUT_SEC) as resp:
            data = json.loads((resp.read() or b"{}").decode("utf-8", "ignore"))
            ip = (data.get("ip") or "").strip()
            return ip or None
    except Exception:
        return None


def _get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return None


def _normalize_kodi_ver(raw):
    text = (raw or "").strip()
    if not text:
        return None
    m = _KODI_VER_RE.search(text)
    return m.group(1) if m else text


def _normalize_row(raw):
    """Zúží starý i nový řádek na slim payload."""
    if not isinstance(raw, dict):
        return None
    ts = raw.get("ts")
    if ts is not None:
        ts = str(ts).strip() or None
    install_id = raw.get("install_id")
    if install_id is not None:
        install_id = str(install_id).strip() or None
    addon_ver = raw.get("addon_ver")
    if addon_ver is not None:
        addon_ver = str(addon_ver).strip() or None
    public_ip = raw.get("public_ip")
    if public_ip is not None:
        public_ip = str(public_ip).strip() or None
    local_ip = raw.get("local_ip")
    if local_ip is not None:
        local_ip = str(local_ip).strip() or None
    return {
        "ts": ts,
        "install_id": install_id,
        "addon_ver": addon_ver,
        "kodi_ver": _normalize_kodi_ver(raw.get("kodi_ver")),
        "public_ip": public_ip,
        "local_ip": local_ip,
    }


def _parse_jsonl_bytes(data):
    rows = []
    if not data:
        return rows
    try:
        text = data.decode("utf-8", "ignore")
    except Exception:
        return rows
    for line in text.splitlines():
        line = (line or "").strip()
        if not line:
            continue
        try:
            raw = json.loads(line)
        except Exception:
            continue
        row = _normalize_row(raw)
        if row:
            rows.append(row)
    return rows


def _read_jsonl_rows(path):
    rows = []
    try:
        if not os.path.exists(path):
            return rows
        with open(path, "r", encoding="utf-8") as f:
            for line in f:
                line = (line or "").strip()
                if not line:
                    continue
                try:
                    raw = json.loads(line)
                except Exception:
                    continue
                row = _normalize_row(raw)
                if row:
                    rows.append(row)
    except Exception:
        pass
    return rows


def _rewrite_jsonl_normalized(path):
    """Přepíše JSONL na slim řádky; vrátí počet řádků. Záloha .bak."""
    if not os.path.exists(path):
        return 0
    rows = _read_jsonl_rows(path)
    bak = path + ".bak"
    try:
        if os.path.exists(bak):
            os.remove(bak)
        os.replace(path, bak)
    except Exception:
        try:
            with open(path, "rb") as src, open(bak, "wb") as dst:
                dst.write(src.read())
        except Exception:
            pass
    with open(path, "w", encoding="utf-8") as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")
    return len(rows)


def _migrate_local_if_needed(addon):
    flag = _path(addon, MIGRATED_V2_FLAG)
    if os.path.exists(flag):
        return
    try:
        _rewrite_jsonl_normalized(_path(addon, START_LOG_FILE))
        _rewrite_jsonl_normalized(_path(addon, PENDING_LOG_FILE))
        _write_text(flag, datetime.now().isoformat(timespec="seconds"))
        log.debug("Telemetry local migrate v2 done.")
    except Exception as ex:
        log.debug("Telemetry local migrate v2 skipped: %s", ex)


def _build_start_row(addon):
    return _normalize_row({
        "ts": datetime.now().astimezone().isoformat(timespec="seconds"),
        "install_id": _get_or_create_install_id(addon),
        "addon_ver": addon.getAddonInfo("version"),
        "kodi_ver": (xbmc.getInfoLabel("System.BuildVersion") or "").strip(),
        "public_ip": _get_public_ip(),
        "local_ip": _get_local_ip(),
    })


def _needs_upload(addon):
    last_path = _path(addon, LAST_UPLOAD_FILE)
    last_raw = _read_text(last_path, "0")
    try:
        last_ts = float(last_raw)
    except Exception:
        last_ts = 0.0
    return (datetime.now().timestamp() - last_ts) >= UPLOAD_INTERVAL_SEC


def _set_uploaded_now(addon):
    _write_text(_path(addon, LAST_UPLOAD_FILE), str(datetime.now().timestamp()))


def _ensure_remote_tree(ftp, path):
    parts = [p for p in (path or "").split("/") if p]
    if not parts:
        return
    cur = ""
    for part in parts:
        cur += "/" + part
        try:
            ftp.mkd(cur)
        except Exception:
            pass


def _ftp_connect():
    from resources.lib.utils.cloud_sync import CloudSync

    ftp = ftplib.FTP()
    ftp.connect(CloudSync.FTP_HOST, CloudSync.FTP_PORT, timeout=FTP_TIMEOUT_SEC)
    ftp.login(CloudSync.FTP_USER, CloudSync.FTP_PASS)
    try:
        ftp.voidcmd("TYPE I")
    except Exception:
        pass
    try:
        ftp.set_pasv(True)
    except Exception:
        pass
    return ftp, CloudSync


def _ftp_quit(ftp):
    if not ftp:
        return
    try:
        ftp.quit()
    except Exception:
        try:
            ftp.close()
        except Exception:
            pass


def _ftp_retr_bytes(ftp, remote_path):
    bio = io.BytesIO()
    try:
        ftp.retrbinary("RETR " + remote_path, bio.write)
        return bio.getvalue()
    except Exception:
        return b""


def _dedupe_rows(rows):
    seen = set()
    out = []
    for row in rows:
        key = (
            row.get("install_id") or "",
            row.get("ts") or "",
            row.get("public_ip") or "",
            row.get("local_ip") or "",
            row.get("addon_ver") or "",
            row.get("kodi_ver") or "",
        )
        if key in seen:
            continue
        seen.add(key)
        out.append(row)
    out.sort(key=lambda r: (r.get("ts") or "", r.get("install_id") or ""))
    return out


def _format_display_ts(ts_raw):
    text = (ts_raw or "").strip()
    if not text:
        return "??-??-???? - ??:??"
    try:
        # ISO s offsetem / Z
        dt = datetime.fromisoformat(text.replace("Z", "+00:00"))
        return dt.strftime("%d-%m-%Y - %H:%M")
    except Exception:
        pass
    # fallback: vezmi YYYY-MM-DDTHH:MM
    m = re.match(r"(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})", text)
    if m:
        return "{}-{}-{} - {}:{}".format(m.group(3), m.group(2), m.group(1), m.group(4), m.group(5))
    return text[:16]


def _format_rows_text(rows):
    if not rows:
        return "Telemetry log je prázdný."
    groups = defaultdict(list)
    for row in rows:
        ip = (row.get("public_ip") or "").strip() or "IP ?"
        groups[ip].append(row)
    parts = []
    for ip in sorted(groups.keys()):
        parts.append("=== {} ===".format(ip))
        for row in sorted(groups[ip], key=lambda r: r.get("ts") or ""):
            dt = _format_display_ts(row.get("ts"))
            ver = row.get("addon_ver") or "?"
            kodi = row.get("kodi_ver") or "?"
            lan = row.get("local_ip") or "?"
            parts.append("{} | ver {} | kodi {} | lan {}".format(dt, ver, kodi, lan))
        parts.append("")
    return "\n".join(parts).rstrip()


def _upload_pending(addon):
    """APPE pending řádků do _telemetry/YYYY-MM.jsonl."""
    _migrate_local_if_needed(addon)
    pending_path = _path(addon, PENDING_LOG_FILE)
    if not os.path.exists(pending_path) or os.path.getsize(pending_path) <= 0:
        return True

    # Normalizuj pending před uploadem (pro jistotu)
    rows = _read_jsonl_rows(pending_path)
    if not rows:
        try:
            os.remove(pending_path)
        except Exception:
            _write_text(pending_path, "")
        return True

    tmp_local = pending_path + ".upload"
    try:
        with open(tmp_local, "w", encoding="utf-8") as f:
            for row in rows:
                f.write(json.dumps(row, ensure_ascii=False) + "\n")
    except Exception as ex:
        log.debug("Telemetry pending normalize failed: %s", ex)
        tmp_local = pending_path

    month_name = datetime.now().strftime("%Y-%m")
    ftp = None
    try:
        ftp, CloudSync = _ftp_connect()
        remote_dir = "{}/_telemetry".format(CloudSync.FTP_ROOT.rstrip("/"))
        remote_file = "{}/{}.jsonl".format(remote_dir, month_name)
        _ensure_remote_tree(ftp, remote_dir)
        with open(tmp_local, "rb") as f:
            ftp.storbinary("APPE " + remote_file, f)
        try:
            os.remove(pending_path)
        except Exception:
            _write_text(pending_path, "")
        if tmp_local != pending_path:
            try:
                os.remove(tmp_local)
            except Exception:
                pass
        log.debug("Telemetry monthly log appended successfully.")
        return True
    except Exception as ex:
        log.debug("Telemetry upload skipped/failed: %s", ex)
        return False
    finally:
        _ftp_quit(ftp)


def log_start_event(addon):
    try:
        _migrate_local_if_needed(addon)
        row = _build_start_row(addon)
        if not row:
            return
        _append_jsonl(_path(addon, START_LOG_FILE), row)
        _append_jsonl(_path(addon, PENDING_LOG_FILE), row)
    except Exception as ex:
        log.debug("Telemetry start log skipped: %s", ex)


def maybe_upload(addon):
    try:
        if not _needs_upload(addon):
            return
        if _upload_pending(addon):
            _set_uploaded_now(addon)
    except Exception as ex:
        log.debug("Telemetry maybe_upload skipped: %s", ex)


def read_local_start_log(addon):
    try:
        _migrate_local_if_needed(addon)
        p = _path(addon, START_LOG_FILE)
        if not os.path.exists(p):
            return "Telemetry log je zatím prázdný (soubor neexistuje)."
        rows = _read_jsonl_rows(p)
        return _format_rows_text(rows)
    except Exception as ex:
        return "Chyba čtení telemetry logu: {}".format(ex)


def _ftp_list_basenames(ftp, remote_dir):
    try:
        names = ftp.nlst(remote_dir)
    except Exception:
        try:
            ftp.cwd(remote_dir)
            names = ftp.nlst()
        except Exception:
            return []
    basenames = []
    for n in names or []:
        base = (n or "").replace("\\", "/").rstrip("/").split("/")[-1]
        if base and base not in (".", ".."):
            basenames.append(base)
    return basenames


def list_cloud_months(addon):
    """Vrátí seznam měsíců YYYY-MM dostupných na FTP (sestupně)."""
    ftp = None
    try:
        ftp, CloudSync = _ftp_connect()
        remote_dir = "{}/_telemetry".format(CloudSync.FTP_ROOT.rstrip("/"))
        months = []
        for base in _ftp_list_basenames(ftp, remote_dir):
            if not base.endswith(".jsonl"):
                continue
            month = base[:-6]
            if _MONTH_RE.match(month):
                months.append(month)
        months = sorted(set(months), reverse=True)
        return months
    except Exception as ex:
        log.debug("Telemetry list months failed: %s", ex)
        return []
    finally:
        _ftp_quit(ftp)


def _pending_rows_for_month(addon, month):
    """Pending řádky; při month filtr jen na daný měsíc (prefix ts)."""
    rows = _read_jsonl_rows(_path(addon, PENDING_LOG_FILE))
    if not month:
        return rows
    prefix = str(month)
    return [r for r in rows if str(r.get("ts") or "").startswith(prefix)]


def read_cloud_start_log(addon, month=None):
    """
    Stáhne _telemetry/YYYY-MM.jsonl a zobrazí formátovaný výpis.
    month=None → všechny měsíce; month='YYYY-MM' → jen daný měsíc.
    Doplní lokální pending (ještě nenahrané starty) pro daný rozsah.
    """
    _migrate_local_if_needed(addon)
    month = (month or "").strip() or None
    if month and not _MONTH_RE.match(month):
        return "Neplatný měsíc: {}".format(month)

    ftp = None
    try:
        ftp, CloudSync = _ftp_connect()
        remote_dir = "{}/_telemetry".format(CloudSync.FTP_ROOT.rstrip("/"))
        basenames = _ftp_list_basenames(ftp, remote_dir)
        month_files = []
        for base in basenames:
            if not base.endswith(".jsonl"):
                continue
            m = base[:-6]
            if not _MONTH_RE.match(m):
                continue
            if month and m != month:
                continue
            month_files.append((m, base))
        month_files.sort(key=lambda x: x[0])

        rows = []
        if not month_files:
            if month:
                cloud_note = "Na FTP není soubor _telemetry/{}.jsonl.".format(month)
            else:
                cloud_note = "Na FTP nejsou žádné _telemetry/YYYY-MM.jsonl."
        else:
            for m, base in month_files:
                data = _ftp_retr_bytes(ftp, "{}/{}".format(remote_dir, base))
                rows.extend(_parse_jsonl_bytes(data))
            if month:
                cloud_note = "Cloud: měsíc {}.".format(month)
            else:
                cloud_note = "Cloud: {} měsíc(ů), soubory {}–{}.".format(
                    len(month_files),
                    month_files[0][0],
                    month_files[-1][0],
                )

        pending_rows = _pending_rows_for_month(addon, month)
        if pending_rows:
            rows.extend(pending_rows)
            cloud_note += " + {} pending lokálně.".format(len(pending_rows))

        rows = _dedupe_rows(rows)
        if not rows:
            return cloud_note + "\n\nŽádné řádky k zobrazení."
        body = _format_rows_text(rows)
        return "{}\nŘádků: {}\n\n{}".format(cloud_note, len(rows), body)
    except Exception as ex:
        log.debug("Telemetry cloud read failed: %s", ex)
        return "Chyba čtení telemetrie z cloudu: {}".format(ex)
    finally:
        _ftp_quit(ftp)


def upload_now(addon):
    try:
        ok = _upload_pending(addon)
        if ok:
            _set_uploaded_now(addon)
        return ok
    except Exception:
        return False


def migrate_cloud_data(addon):
    """
    Sloučí all_users-* + per-install start-* do _telemetry/YYYY-MM.jsonl.
    Staré soubory nemaže. Vrací (ok: bool, message: str).
    """
    _migrate_local_if_needed(addon)
    ftp = None
    try:
        ftp, CloudSync = _ftp_connect()
        remote_dir = "{}/_telemetry".format(CloudSync.FTP_ROOT.rstrip("/"))
        _ensure_remote_tree(ftp, remote_dir)

        basenames = _ftp_list_basenames(ftp, remote_dir)
        if not basenames:
            # ověř že listování vůbec funguje (prázdný dir vs. chyba)
            try:
                ftp.nlst(remote_dir)
            except Exception:
                try:
                    ftp.cwd(remote_dir)
                    ftp.nlst()
                except Exception as ex:
                    return False, "Nelze listovat _telemetry: {}".format(ex)

        by_month = defaultdict(list)
        months_touched = set()

        # all_users-YYYY-MM.jsonl + už existující YYYY-MM.jsonl
        for base in basenames:
            m = _ALL_USERS_RE.match(base)
            if m:
                month = m.group(1)
                data = _ftp_retr_bytes(ftp, "{}/{}".format(remote_dir, base))
                by_month[month].extend(_parse_jsonl_bytes(data))
                months_touched.add(month)
                continue
            if base.endswith(".jsonl"):
                month_candidate = base[:-6]
                if _MONTH_RE.match(month_candidate):
                    data = _ftp_retr_bytes(ftp, "{}/{}".format(remote_dir, base))
                    by_month[month_candidate].extend(_parse_jsonl_bytes(data))
                    months_touched.add(month_candidate)

        # per-install složky
        for base in basenames:
            if "." in base and base.endswith(".jsonl"):
                continue
            # přeskoč soubory
            if base.endswith(".jsonl") or base.endswith(".part") or base.endswith(".txt"):
                continue
            sub = "{}/{}".format(remote_dir, base)
            try:
                sub_names = ftp.nlst(sub)
            except Exception:
                continue
            for sn in sub_names or []:
                sbase = (sn or "").replace("\\", "/").rstrip("/").split("/")[-1]
                sm = _START_MONTH_RE.match(sbase)
                if not sm:
                    continue
                month = sm.group(1)
                data = _ftp_retr_bytes(ftp, "{}/{}".format(sub, sbase))
                by_month[month].extend(_parse_jsonl_bytes(data))
                months_touched.add(month)

        if not months_touched:
            return True, "Na FTP nebyla nalezena stará telemetry data ke sloučení."

        written = 0
        total_rows = 0
        for month in sorted(months_touched):
            rows = _dedupe_rows(by_month.get(month) or [])
            if not rows:
                continue
            total_rows += len(rows)
            payload = ("\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n").encode("utf-8")
            remote_file = "{}/{}.jsonl".format(remote_dir, month)
            tmp_file = remote_file + ".part"
            bio = io.BytesIO(payload)
            ftp.storbinary("STOR " + tmp_file, bio)
            try:
                ftp.rename(tmp_file, remote_file)
            except Exception:
                try:
                    ftp.sendcmd("RNFR " + tmp_file)
                    ftp.sendcmd("RNTO " + remote_file)
                except Exception:
                    # fallback přímý STOR
                    bio2 = io.BytesIO(payload)
                    ftp.storbinary("STOR " + remote_file, bio2)
            written += 1

        msg = (
            "Migrace OK: {} měsíc(ů), {} unikátních řádků → _telemetry/YYYY-MM.jsonl.\n"
            "Staré all_users-* / per-install soubory zůstaly (smaž ručně po kontrole)."
        ).format(written, total_rows)
        return True, msg
    except Exception as ex:
        log.debug("Telemetry cloud migrate failed: %s", ex)
        return False, "Migrace cloudu selhala: {}".format(ex)
    finally:
        _ftp_quit(ftp)
