"""Approval workflow — propose → notify with signed link → approve → apply.

Security model:
  • Approval links carry an HMAC-signed token bound to one approval id
    with an expiry (default 72h). Guessing or tampering fails verification.
  • The signing secret comes from env APP_SECRET (set this in the cloud!);
    if unset, a random one is generated and stored in .app_secret so links
    survive restarts on a single machine.
  • Links are single-decision: once approved/rejected, the token is dead.

Notifications:
  • Email via SMTP  — env: SMTP_HOST, SMTP_PORT (587), SMTP_USER,
    SMTP_PASS, MAIL_FROM, MAIL_TO
  • Slack via webhook — env: SLACK_WEBHOOK
  • Both configured -> both fire. Neither -> link is printed to the log.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import smtplib
import time
from datetime import datetime
from email.mime.text import MIMEText
from pathlib import Path

import requests

from .db import DB

APP_DIR = Path(__file__).parent.parent
TOKEN_TTL_HOURS = 72


# ------------------------- signing -------------------------

def _secret() -> bytes:
    s = os.environ.get("APP_SECRET")
    if s:
        return s.encode()
    f = APP_DIR / ".app_secret"
    if not f.exists():
        f.write_text(base64.urlsafe_b64encode(os.urandom(32)).decode(), encoding="utf-8")
    return f.read_text(encoding="utf-8").strip().encode()


def make_token(approval_id: int) -> str:
    exp = int(time.time()) + TOKEN_TTL_HOURS * 3600
    payload = f"{approval_id}.{exp}"
    sig = hmac.new(_secret(), payload.encode(), hashlib.sha256).hexdigest()[:32]
    return base64.urlsafe_b64encode(f"{payload}.{sig}".encode()).decode()


def verify_token(token: str) -> int | None:
    """Returns approval_id if valid and unexpired, else None."""
    try:
        raw = base64.urlsafe_b64decode(token.encode()).decode()
        aid, exp, sig = raw.rsplit(".", 2)
        payload = f"{aid}.{exp}"
        want = hmac.new(_secret(), payload.encode(), hashlib.sha256).hexdigest()[:32]
        if not hmac.compare_digest(sig, want):
            return None
        if int(exp) < time.time():
            return None
        return int(aid)
    except Exception:
        return None


# ------------------------- persistence -------------------------

def create_approval(db: DB, site: str, summary: dict, diff: str,
                    kind: str = "remediation") -> int:
    return db.insert(
        "INSERT INTO approvals (site, created_at, status, summary, diff, kind) "
        "VALUES (?,?,?,?,?,?)",
        (site, datetime.now().isoformat(timespec="seconds"), "pending",
         json.dumps(summary), diff[:400_000], kind))


def get_approval(db: DB, aid: int) -> dict | None:
    rows = db.q("SELECT id, site, created_at, status, summary, diff, "
                "decided_at, note, kind FROM approvals WHERE id=?", (aid,))
    if not rows:
        return None
    r = rows[0]
    return {"id": r[0], "site": r[1], "created_at": r[2], "status": r[3],
            "summary": json.loads(r[4]), "diff": r[5] or "",
            "decided_at": r[6], "note": r[7], "kind": r[8] or "remediation"}


def set_status(db: DB, aid: int, status: str, note: str = ""):
    db.q("UPDATE approvals SET status=?, decided_at=?, note=? WHERE id=?",
         (status, datetime.now().isoformat(timespec="seconds"), note, aid))


def has_pending(db: DB, site: str, kind: str = "remediation") -> bool:
    rows = db.q("SELECT COUNT(*) FROM approvals WHERE site=? AND status='pending' "
                "AND kind=?", (site, kind))
    return bool(rows and rows[0][0])


# ------------------------- notification -------------------------

def notify(site: str, summary: dict, url: str, log=print, kind: str = "remediation"):
    n_items = len(summary)
    fixes = sorted({f for lst in summary.values() for f in lst})
    if kind == "content":
        subject = (f"[SEO Optimizer] {site}: {n_items} content update(s) drafted "
                  f"— approval needed")
        body = (f"The SEO Optimizer drafted {n_items} content update(s) for {site} "
                f"(Google Business Profile / social).\n\n"
                f"Items: {', '.join(fixes)}\n\n"
                f"Review the exact drafted text and approve or reject here (link "
                f"valid {TOKEN_TTL_HOURS}h):\n{url}\n\n"
                f"Nothing is posted until you approve. Items marked 'draft only' "
                f"are never auto-posted — copy that text yourself.")
    else:
        subject = f"[SEO Optimizer] {site}: {n_items} file(s) have proposed fixes — approval needed"
        body = (f"The SEO Optimizer proposes changes to {n_items} file(s) on {site}.\n\n"
                f"Fix types: {', '.join(fixes)}\n\n"
                f"Review the exact diff and approve or reject here (link valid "
                f"{TOKEN_TTL_HOURS}h):\n{url}\n\n"
                f"Nothing is uploaded until you approve. A full backup is taken "
                f"before any change, and everything is rollback-able.")

    sent = []
    smtp_host = os.environ.get("SMTP_HOST")
    smtp_attempted = bool(smtp_host)
    if smtp_host:
        try:
            msg = MIMEText(body)
            msg["Subject"] = subject
            msg["From"] = os.environ.get("MAIL_FROM", os.environ.get("SMTP_USER", ""))
            msg["To"] = os.environ["MAIL_TO"]
            with smtplib.SMTP(smtp_host,
                              int(os.environ.get("SMTP_PORT", "587"))) as s:
                s.starttls()
                if os.environ.get("SMTP_USER"):
                    s.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
                s.send_message(msg)
            sent.append("email")
        except Exception as e:
            log(f"Email notification failed (SMTP was configured, but the "
                f"send itself failed): {e}")

    hook = os.environ.get("SLACK_WEBHOOK")
    slack_attempted = bool(hook)
    if hook:
        try:
            requests.post(hook, json={"text": f"*{subject}*\n{url}"}, timeout=15)
            sent.append("slack")
        except Exception as e:
            log(f"Slack notification failed (webhook was configured, but "
                f"the send itself failed): {e}")

    if sent:
        log(f"Approval link sent via {', '.join(sent)}.")
    elif smtp_attempted or slack_attempted:
        log(f"Email/Slack were configured but the send failed \u2014 "
            f"approval link: {url}")
    else:
        log(f"No SMTP/Slack configured — approval link: {url}")
    return sent
