"""Shared config and plumbing. Everything else imports from here."""
import json
import os
import pathlib
import re
import sys

ROOT = pathlib.Path(__file__).parent
DATA = ROOT / "data"
RAW = DATA / "raw"

# The four niches. Keys are used as ids everywhere (filenames, jsonl fields,
# creators.yaml sections), so changing one means re-running from Phase 1.
NICHES = {
    "wellness": "mental wellness and motivational quotes",
    "printing3d": "3D printing",
    "finance": "personal finance and investing",
    "consumertech": "long-term consumer technology",
}

# Phase 2 curation knobs.
MIN_AGE_DAYS = 14        # views haven't settled before this
MIN_POSTS_PER_CREATOR = 10   # below this there's no stable baseline to compare against
WINNER_PERCENTILE = 0.82     # keep the top ~18%
HOLDOUT_SIZE = 300

# Short-form only.
MAX_DURATION_S = 180

TEACHER_MODEL = "gpt-5.4-mini"
EMBED_MODEL = "text-embedding-3-small"


# The prompt the distilled model is trained against. serve/generate.py imports the
# same two functions, so training and inference can never drift apart — if you edit
# this, the existing checkpoint is stale.
PACK_SYSTEM = (
    "You are a short-form content writer. Given a brief, produce one piece of "
    "content adapted for YouTube Shorts, Instagram Reels and Pinterest.\n"
    "Reply with a single JSON object and nothing else, with these keys:\n"
    '  hook          - the opening line, written to stop a scroll\n'
    '  script        - 30-60 seconds of spoken script, short beats on separate lines\n'
    '  youtube       - {title, description, tags[]}\n'
    '  instagram     - {caption, hashtags[]}\n'
    '  pinterest     - {title, description}\n'
    '  long_caption  - a standalone written version for carousels or threads\n'
    "Write in the register of the niche. Be concrete and specific. Never pad."
)


def _obj(props, req):
    return {"type": "object", "properties": props, "required": req,
            "additionalProperties": False}


_STR = {"type": "string"}
_STRS = {"type": "array", "items": {"type": "string"}}

# One schema, used three ways: to constrain the teacher during backtranslation, to
# constrain Ollama at inference, and as the contract validate_pack() checks against.
PACK_SCHEMA = _obj({
    "hook": _STR,
    "script": _STR,
    "youtube": _obj({"title": _STR, "description": _STR, "tags": _STRS},
                    ["title", "description", "tags"]),
    "instagram": _obj({"caption": _STR, "hashtags": _STRS}, ["caption", "hashtags"]),
    "pinterest": _obj({"title": _STR, "description": _STR}, ["title", "description"]),
    "long_caption": _STR,
}, ["hook", "script", "youtube", "instagram", "pinterest", "long_caption"])


def render_brief(brief, niche=""):
    """Brief -> the user turn. Must produce identical text at train and inference time."""
    parts = []
    if niche:
        parts.append(f"NICHE: {niche}")
    parts.append(f"TOPIC: {brief.get('topic','')}")
    parts.append(f"AUDIENCE: {brief.get('audience','')}")
    parts.append(f"ANGLE: {brief.get('angle','')}")
    ctx = brief.get("research_context") or []
    if ctx:
        parts.append("RESEARCH:")
        parts.extend(f"- {c}" for c in ctx)
    if brief.get("format_note"):
        parts.append(f"FORMAT: {brief['format_note']}")
    return "\n".join(parts)


STOPWORDS = set(
    "the a an and or but if then to of in on for with is are was were be been this "
    "that these those it its you your i we they he she as at by from not so do does "
    "did how why what when who will can just get got make made my me our".split())


def words(s):
    """Content tokens. Used for near-duplicate detection, cross-platform matching
    and retrieval — one tokeniser everywhere so those three agree with each other."""
    return {w for w in re.findall(r"[a-z0-9']+", (s or "").lower())
            if len(w) > 2 and w not in STOPWORDS}


# Platform limits, as published. Exceeding these means the post is silently
# truncated by the platform, so a pack that violates one is simply broken.
LIMITS = {
    "youtube.title": 100,
    "youtube.description": 5000,
    "instagram.caption": 2200,
    "pinterest.title": 100,
    "pinterest.description": 800,
}


def validate_pack(pack):
    """-> list of problems. Empty list means the pack is publishable."""
    problems = []
    if not isinstance(pack, dict):
        return ["not a JSON object"]

    for key in ("hook", "script", "long_caption"):
        if not str(pack.get(key, "")).strip():
            problems.append(f"{key} is empty")

    shape = {"youtube": ("title", "description", "tags"),
             "instagram": ("caption", "hashtags"),
             "pinterest": ("title", "description")}
    for platform, fields in shape.items():
        block = pack.get(platform)
        if not isinstance(block, dict):
            problems.append(f"{platform} missing")
            continue
        for f in fields:
            val = block.get(f)
            if val in (None, "", []):
                problems.append(f"{platform}.{f} is empty")
            elif isinstance(val, str):
                cap = LIMITS.get(f"{platform}.{f}")
                if cap and len(val) > cap:
                    problems.append(f"{platform}.{f} is {len(val)} chars, limit {cap}")

    tags = (pack.get("youtube") or {}).get("tags") or []
    if isinstance(tags, list) and sum(len(str(t)) + 1 for t in tags) > 500:
        problems.append("youtube.tags exceed 500 characters total")
    return problems


def load_env():
    """Read .env into os.environ without adding a dependency. Existing vars win."""
    f = ROOT / ".env"
    if not f.exists():
        return
    for line in f.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        os.environ.setdefault(k.strip(), v.strip().strip("'\""))


def need(var):
    load_env()
    val = os.environ.get(var)
    if not val:
        sys.exit(f"{var} is not set. Copy .env.example to .env and fill it in.")
    return val


def read_jsonl(path):
    path = pathlib.Path(path)
    if not path.exists():
        return []
    out = []
    for line in path.read_text().splitlines():
        if line.strip():
            out.append(json.loads(line))
    return out


def append_jsonl(path, rows):
    """Checkpoint helper. Collectors call this after every page so a crash or a
    quota wall never costs more than the page in flight."""
    path = pathlib.Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a") as fh:
        for r in rows:
            fh.write(json.dumps(r, ensure_ascii=False) + "\n")


def write_jsonl(path, rows):
    path = pathlib.Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w") as fh:
        for r in rows:
            fh.write(json.dumps(r, ensure_ascii=False) + "\n")


def seen_ids(path, key="id"):
    """Ids already on disk, so collectors resume instead of re-fetching."""
    return {r[key] for r in read_jsonl(path) if key in r}
