import os
import re
import time
from datetime import datetime, timezone
from urllib.parse import urlparse, urlencode

import discord
import aiohttp

from modules.common.settings import cfg

# ---------- Version helpers ----------

_VERSION_RE = re.compile(r'\b\d+\.\d+\.\d+\.\d+(?:\.[A-Za-z0-9]+)?\b')

def _extract_version(subject: str) -> str | None:
    if not subject:
        return None
    m = _VERSION_RE.search(subject)
    return m.group(0) if m else None

def _split_subject_body(full_message: str) -> tuple[str | None, str | None]:
    if not full_message:
        return None, None
    lines = [ln.rstrip() for ln in full_message.splitlines()]
    subject = None
    i = 0
    while i < len(lines) and subject is None:
        if lines[i].strip():
            subject = lines[i].strip()
        i += 1
    body = '\n'.join(lines[i:]).strip() if i < len(lines) else ''
    return subject or None, (body or None)

def _cmp_versions(a: str | None, b: str | None) -> int:
    """Compare 1.2.3.4.a2 style; if either missing, treat as equal (0)."""
    if not a or not b:
        return 0
    pa, pb = a.split('.'), b.split('.')
    while len(pa) < 5: pa.append('0')
    while len(pb) < 5: pb.append('0')

    def key(x: str):
        if x.isdigit():
            return (int(x), '', 1)
        m = re.match(r'(\d+)(.*)', x)
        if m:
            return (int(m.group(1)), m.group(2), 2)
        return (0, x, 3)

    for xa, xb in zip(pa, pb):
        ka, kb = key(xa), key(xb)
        if ka[0] != kb[0]:
            return 1 if ka[0] > kb[0] else -1
        if ka[2] != kb[2]:
            return 1 if ka[2] < kb[2] else -1
        if ka[1] != kb[1]:
            return 1 if ka[1] > kb[1] else -1
    return 0

# ---------- Gitea helpers ----------

def _parse_repo_url(repo_url: str) -> tuple[str | None, str | None, str | None]:
    """
    From https://host/owner/repo(.git) -> (api_base, owner, repo)
    api_base = https://host/api/v1
    """
    try:
        pr = urlparse(repo_url.strip().rstrip('/'))
        parts = [p for p in pr.path.split('/') if p]
        if len(parts) >= 2:
            owner, repo = parts[0], parts[1]
            if repo.endswith('.git'):
                repo = repo[:-4]
            api_base = f"{pr.scheme}://{pr.netloc}/api/v1"
            return api_base, owner, repo
    except Exception:
        pass
    return None, None, None

def _auth_headers_from_cfg(r):
    """
    Build Authorization header using repo auth tokens.
    - Preferred: cfg('repo_ahtoken') (comes from settings.json or env REPO_AHTOKEN)
    - Fallbacks: GITEA_TOKEN / GITEA_USER envs (non-SHAI)
    """
    ahtoken = r.get('repo_ahtoken', '').strip()  # REPO_AHTOKEN via settings/env
    if ahtoken:
        if not ahtoken.lower().startswith('token '):
            ahtoken = f"token {ahtoken}"
        return {"Authorization": ahtoken}

    tok = os.getenv("GITEA_TOKEN", "").strip()
    usr = os.getenv("GITEA_USER", "").strip()
    if tok and usr:
        import base64
        b64 = base64.b64encode(f"{usr}:{tok}".encode()).decode()
        return {"Authorization": f"Basic {b64}"}
    if tok:
        return {"Authorization": f"token {tok}"}
    return {}

async def _http_json(url: str, headers: dict, timeout_sec: int = 10):
    import aiohttp
    timeout = aiohttp.ClientTimeout(total=timeout_sec)
    async with aiohttp.ClientSession(timeout=timeout, headers=headers or {}) as sess:
        async with sess.get(url) as resp:
            ctype = resp.headers.get("Content-Type", "")
            if resp.status != 200:
                text = await resp.text()
                raise RuntimeError(f"Gitea GET {url} -> {resp.status} ({ctype}): {text[:200]}")
            if "application/json" not in ctype:
                text = await resp.text()
                raise RuntimeError(f"Gitea GET {url} non-JSON {ctype}: {text[:200]}")
            return await resp.json()

async def _fetch_latest_commit(api_base: str, owner: str, repo: str, branch: str | None,
                               headers: dict) -> tuple[str | None, str | None, str | None]:
    """
    Returns (sha, subject, body) for latest commit using list-commits:
    /api/v1/repos/{owner}/{repo}/commits?sha=main&stat=false&verification=false&files=false&limit=1
    If branch is falsy, omit 'sha' to use server default.
    """
    from urllib.parse import urlencode
    params = {
        "stat": "false",
        "verification": "false",
        "files": "false",
        "limit": "1",
    }
    if branch:
        params["sha"] = branch
    url = f"{api_base}/repos/{owner}/{repo}/commits?{urlencode(params)}"

    data = await _http_json(url, headers)
    if not isinstance(data, list) or not data:
        raise RuntimeError("Commits list empty or invalid")

    latest = data[0]
    sha = latest.get("sha") or latest.get("id")
    message = ""
    commit_obj = latest.get("commit") or {}
    if isinstance(commit_obj, dict):
        message = commit_obj.get("message") or ""

    subject, body = _split_subject_body(message or "")
    return sha, (subject or ""), (body or "")

# ---------- Boot reason inference ----------

def _is_near_scheduled(now_utc: datetime, hhmm_utc: str | None, window_min: int = 5) -> bool:
    if not hhmm_utc:
        return False
    try:
        hh, mm = [int(x) for x in hhmm_utc.strip().split(':', 1)]
    except Exception:
        return False
    sched = now_utc.replace(hour=hh, minute=mm, second=0, microsecond=0)
    return abs((now_utc - sched).total_seconds()) <= window_min * 60

def _format_status_line(kind: str, old_ver: str | None, new_ver: str | None) -> str:
    if kind == "updated":
        return f"✅ Updated from **{old_ver or 'unknown'}** → **{new_ver or 'unknown'}**"
    if kind == "scheduled":
        return "🕒 Scheduled restart executed"
    if kind == "manual":
        return "🟢 Manual restart detected"
    if kind == "rollback":
        return f"⚠️ Version rollback detected: **{old_ver or 'unknown'}** → **{new_ver or 'unknown'}**"
    return "🟢 Bot started"

# ---------- Main entry ----------

async def post_boot_notice(bot):
    """
    Always posts a startup status to the modlog channel.
    - If version changed (update or rollback): post status + full commit message.
    - If NO version change (manual/scheduled): post status ONLY, but append the running version to that status.
    """
    try:
        await bot.wait_until_ready()
    except Exception as e:
        print(f"[boot_notice] wait_until_ready failed: {e}")

    for guild in bot.guilds:
        print(f'  - {guild.name} (id: {guild.id})')

    r = cfg(bot)
    modlog_channel_id = r.int('modlog_channel_id', 0)
    if not modlog_channel_id:
        print("[boot_notice] modlog_channel_id not configured; skipping.")
        return

    ch = bot.get_channel(modlog_channel_id)
    if not ch:
        for g in bot.guilds:
            ch = g.get_channel(modlog_channel_id)
            if ch:
                break
    if not ch:
        print(f"[boot_notice] channel id {modlog_channel_id} not found; skipping.")
        return

    repo_url = r.get('repo_url', '')
    branch = r.get('repo_branch', 'main') or None
    check_time_utc = r.get('check_time_utc', '')
    headers = _auth_headers_from_cfg(r)

    api_base = owner = repo = None
    if repo_url:
        api_base, owner, repo = _parse_repo_url(repo_url)
        if not all([api_base, owner, repo]):
            print(f"[boot_notice] failed to parse repo_url={repo_url!r}")
    else:
        print("[boot_notice] repo_url missing; commit lookup skipped.")

    # State
    dm = getattr(bot, "data_manager", None)
    if not dm:
        print("[boot_notice] data_manager missing on bot; cannot persist state.")
        return
    prev = (dm.get('boot_state') or [{}])[-1] if dm.get('boot_state') else {}
    prev_sha = prev.get('last_sha') or None
    prev_ver = prev.get('last_version') or None

    # Fetch latest commit
    sha = subject = body = None
    if api_base and owner and repo:
        try:
            sha, subject, body = await _fetch_latest_commit(api_base, owner, repo, branch, headers)
        except Exception as e:
            print(f"[boot_notice] fetch latest commit failed: {e}")
    else:
        print("[boot_notice] repo parsing failed; commit lookup skipped.")

    curr_ver = _extract_version(subject) if subject else None

    # Decide reason
    now_utc = datetime.now(timezone.utc)
    if prev_ver and curr_ver:
        cmpv = _cmp_versions(prev_ver, curr_ver)
        if cmpv < 0:
            reason, ping_owner = "updated", False
        elif cmpv > 0:
            reason, ping_owner = "rollback", True
        else:
            reason, ping_owner = ("scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"), False
    else:
        if prev_sha and sha and prev_sha != sha:
            reason, ping_owner = "updated", False
        else:
            reason, ping_owner = ("scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"), False

    # Build + post status line
    status_line = _format_status_line(reason, prev_ver, curr_ver)

    # If no version change (manual/scheduled), append the running version to the status line,
    # and DO NOT post the commit message separately.
    append_version_only = reason in ("manual", "scheduled")
    if append_version_only and curr_ver:
        status_line = f"{status_line} — running **{curr_ver}**"

    try:
        allowed = discord.AllowedMentions(
            everyone=False,
            users=True if (ping_owner and ch.guild and ch.guild.owner_id) else False,
            roles=False,
            replied_user=False
        )
        if ping_owner and ch.guild and ch.guild.owner_id:
            status_line = f"{status_line}\n<@{ch.guild.owner_id}>"
        await ch.send(status_line, allowed_mentions=allowed)
    except Exception as e:
        print(f"[boot_notice] failed to send status line: {e}")
        return

    # Only post commit message if version CHANGED (updated or rollback)
    if not append_version_only:
        try:
            title = (curr_ver or subject or "Latest commit").strip()
            if title or body:
                commit_msg = f"**{title}**\n{body}" if body else f"**{title}**"
                await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none())
        except Exception as e:
            print(f"[boot_notice] failed to send commit message: {e}")

    # Persist state
    try:
        dm.add('boot_state', {
            'last_sha': sha,
            'last_version': curr_ver,
            'last_subject': subject,
            'last_boot_ts': time.time(),
        })
    except Exception as e:
        print(f"[boot_notice] failed to persist boot_state: {e}")
