#!/usr/bin/env python3
"""Internship map backend: stdlib-only JSON API + static frontend.

    python3 app/server.py [--port 8000] [--host 127.0.0.1]

Data is joined once at startup from forms/ (companies + geocode) and
forms/extracted/ (D2 forms, tagged projects).
"""
import argparse, csv, json, mimetypes, re, sys, unicodedata
from collections import Counter, defaultdict
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from urllib.parse import urlparse, parse_qs, unquote

import benefits

ROOT = Path(__file__).resolve().parent.parent
STATIC = Path(__file__).resolve().parent / "static"
FORMS = ROOT / "forms"
EXT = FORMS / "extracted"

LANG_LABELS = {"cpp": "C++", "csharp": "C#", "c": "C", "sql": "SQL", "css": "CSS", "html": "HTML",
               "php": "PHP", "javascript": "JavaScript", "typescript": "TypeScript", "vhdl": "VHDL",
               "systemverilog": "SystemVerilog", "powershell": "PowerShell", "matlab": "MATLAB",
               "json": "JSON", "yaml": "YAML", "vba": "VBA", "capl": "CAPL", "qml": "QML"}
TOPIC_LABELS = {"ai": "AI", "nlp": "NLP", "iot": "IoT", "gis": "GIS", "ar-vr": "AR/VR", "testing-qa": "Testing/QA",
                "cloud-devops": "Cloud/DevOps", "computer-vision": "Computer vision", "chip-design": "Chip design",
                "enterprise-systems": "Enterprise systems"}


BENEFIT_LABELS = dict(benefits.BENEFITS)


def fold(s):
    """Lowercase, strip diacritics (Vietnamese đ included) for accent-insensitive search."""
    s = unicodedata.normalize("NFD", (s or "").lower().replace("đ", "d"))
    return re.sub(r"[^a-z0-9]+", " ", "".join(c for c in s if unicodedata.category(c) != "Mn")).strip()


def label(kind, slug):
    if kind == "topic":
        return TOPIC_LABELS.get(slug, slug.replace("-", " ").capitalize())
    if kind == "lang":
        return LANG_LABELS.get(slug, slug.capitalize())
    if kind == "benefit":
        return BENEFIT_LABELS.get(slug, slug)
    return slug


def load():
    companies = {c["id"]: c for c in json.load(open(FORMS / "company_info.json"))}
    for r in csv.DictReader(open(FORMS / "company_info_geocoded.csv", encoding="utf-8")):
        c = companies.get(r["id"])
        if c:
            c["lat"] = float(r["latitude"]) if r["latitude"] else None
            c["lon"] = float(r["longitude"]) if r["longitude"] else None
            c["geo_method"] = r["method"]
    d2 = json.load(open(EXT / "d2.json"))
    tags = json.load(open(EXT / "project_tags.json"))

    forms = defaultdict(list)
    for f in d2:
        if f.get("fields") and f.get("company_id") in companies:
            forms[f["company_id"]].append(f)
    projects = defaultdict(list)
    for p in tags:
        projects[p["company_id"]].append(p)

    out = []
    for cid, c in companies.items():
        fl = sorted(forms.get(cid, []), key=lambda f: f.get("form_date") or "", reverse=True)

        def first(key):  # latest non-empty value across this company's D2 forms
            for f in fl:
                v = f["fields"].get(key)
                if v:
                    return v
        # timelines from the form text, keyed by folded title
        timelines = {}
        for f in fl:
            for p in f["fields"].get("projects") or []:
                timelines.setdefault(fold(p.get("title")), p)
        projs = []
        for p in sorted(projects.get(cid, []), key=lambda p: p["titles"][0] if p["titles"] else ""):
            title = p["titles"][0] if p["titles"] else "(untitled)"
            raw = timelines.get(fold(title), {})
            projs.append({"title": title, "weeks": p.get("weeks"), "topics": p["topics"],
                          "languages": p["languages"], "frameworks": p["frameworks"],
                          "raw_languages": raw.get("languages") or [], "raw_frameworks": raw.get("frameworks") or [],
                          "timeline": raw.get("timeline") or []})
        topics = sorted({t for p in projs for t in p["topics"]})
        langs = sorted({t for p in projs for t in p["languages"]})
        fws = sorted({t for p in projs for t in p["frameworks"]})
        contact = first("contact") or {}
        btext = first("benefits")
        dur = first("duration")
        wk = re.search(r"(\d+)\s*(?:consecutive\s+)?(?:tuần|weeks?)", dur, re.I) if isinstance(dur, str) else None
        stipend = benefits.stipend_amount(btext if isinstance(btext, str) else "", fold, int(wk.group(1)) if wk else None)
        btags = sorted(benefits.tag_benefits(btext if isinstance(btext, str) else json.dumps(btext, ensure_ascii=False)
                                             if btext else "", fold), key=[k for k, _ in benefits.BENEFITS].index)
        rec = {
            "id": cid, "name": c["shortname"], "fullname": c["fullname"], "address": c["location"],
            "lat": c.get("lat"), "lon": c.get("lon"), "geo_method": c.get("geo_method"),
            "max_slot": c["max_slot"], "accepted": c["student_accepted"], "acceptance": c["acceptance"],
            "topics": topics, "languages": langs, "frameworks": fws, "benefit_tags": btags, "stipend": stipend,
            "semesters": sorted({f["fields"].get("semester") for f in fl if f["fields"].get("semester")}),
            "website": first("website"), "email": first("email"), "phone": first("phone"),
            "intro": first("intro"), "period": first("period"), "benefits": btext,
            "requirements": first("requirements") or [], "work_address": first("work_address"),
            "deadline": first("deadline"), "contact": contact, "tax_code": first("tax_code"),
            "projects": projs, "forms": len(fl),
        }
        # search index: weighted folded fields
        rec["_idx"] = [
            (4, fold(f"{rec['name']} {rec['fullname']}")),
            (2, fold(" ".join(p["title"] for p in projs))),
            (2, fold(" ".join(label("topic", t) + " " + t for t in topics) + " " +
                     " ".join(label("lang", t) for t in langs) + " " + " ".join(fws) + " " +
                     " ".join(label("benefit", t) + " " + t for t in btags) + " " +
                     " ".join(" ".join(p["raw_languages"] + p["raw_frameworks"]) for p in projs))),
            (1.5, fold(rec["address"] + " " + (rec["work_address"] or ""))),
            (1, fold(" ".join(str(x or "") for x in [rec["intro"], rec["benefits"], rec["website"], rec["email"],
                               rec["tax_code"], " ".join(map(str, rec["requirements"])),
                               " ".join(str(t.get("task") or "") for p in projs for t in p["timeline"])]))),
        ]
        out.append(rec)
    return out


DATA = load()
BY_ID = {c["id"]: c for c in DATA}
FACETS = {
    "topic": lambda c: c["topics"], "lang": lambda c: c["languages"], "fw": lambda c: c["frameworks"],
    "benefit": lambda c: c["benefit_tags"],
    "acceptance": lambda c: [c["acceptance"]], "semester": lambda c: c["semesters"],
}


def score(c, tokens):
    total = 0.0
    for t in tokens:
        best = 0.0
        for w, text in c["_idx"]:
            if t in text:
                best = max(best, w)
        if not best:
            return 0.0
        total += best
    return total


def query(params):
    q = fold(params.get("q", [""])[0])
    tokens = q.split()
    match_all = params.get("match", ["any"])[0] == "all"
    has_loc = params.get("located", ["0"])[0] == "1"
    smin, smax = (float(params.get(k, [""])[0] or d) for k, d in (("smin", 0), ("smax", "inf")))
    by_stipend = params.get("sort", [""])[0] == "stipend"
    sel = {k: [v for v in params.get(k, [""])[0].split(",") if v] for k in FACETS}

    def ok(c, skip=None):
        if has_loc and c["lat"] is None:
            return False
        if smin > 0 or smax != float("inf"):   # amount filter: stated stipend range overlaps [smin, smax]
            st = c["stipend"]
            if not st or st["max"] < smin or st["min"] > smax:
                return False
        for k, vals in sel.items():
            if k == skip or not vals:
                continue
            have = set(FACETS[k](c))
            if not (set(vals) <= have if match_all and k in ("topic", "lang", "fw", "benefit") else have & set(vals)):
                return False
        return True

    scored = [(score(c, tokens) if tokens else 1.0, c) for c in DATA]
    scored = [(s, c) for s, c in scored if s > 0]
    results = [(s, c) for s, c in scored if ok(c)]
    # facet counts: each facet counted over rows matching every *other* filter
    counts = {}
    for k in FACETS:
        cnt = Counter()
        for s, c in scored:
            if ok(c, skip=k):
                cnt.update(set(FACETS[k](c)))
        counts[k] = dict(cnt)
    if by_stipend:   # highest stated stipend first; companies without an amount last
        results.sort(key=lambda sc: (-(sc[1]["stipend"]["max"] if sc[1]["stipend"] else -1), sc[1]["name"].lower()))
    else:
        results.sort(key=lambda sc: (-sc[0], sc[1]["name"].lower()))
    brief = [{"id": c["id"], "name": c["name"], "fullname": c["fullname"], "address": c["address"],
              "lat": c["lat"], "lon": c["lon"], "acceptance": c["acceptance"], "max_slot": c["max_slot"],
              "accepted": c["accepted"], "topics": c["topics"], "languages": c["languages"],
              "frameworks": c["frameworks"][:8], "benefit_tags": c["benefit_tags"], "stipend": c["stipend"], "n_projects": len(c["projects"])} for _, c in results]
    return {"total": len(brief), "located": sum(1 for b in brief if b["lat"] is not None),
            "companies": brief, "counts": counts}


def meta():
    facets = {}
    for k, getter in (("topic", "topics"), ("lang", "languages"), ("fw", "frameworks")):
        cnt = Counter(t for c in DATA for t in c[getter])
        facets[k] = [{"value": v, "label": label(k, v), "count": n} for v, n in cnt.most_common()]
    bc = Counter(t for c in DATA for t in c["benefit_tags"])
    facets["benefit"] = [{"value": v, "label": l, "count": bc[v]} for v, l in benefits.BENEFITS if bc[v]]
    amounts = sorted(c["stipend"]["max"] for c in DATA if c["stipend"])
    return {"facets": facets, "total": len(DATA),
            "stipend": {"n": len(amounts), "max": amounts[-1] if amounts else 0,
                        "median": amounts[len(amounts) // 2] if amounts else 0},
            "located": sum(1 for c in DATA if c["lat"] is not None),
            "semesters": sorted({s for c in DATA for s in c["semesters"]}),
            "acceptance": sorted({c["acceptance"] for c in DATA})}


class H(BaseHTTPRequestHandler):
    def send(self, code, body, ctype="application/json; charset=utf-8"):
        if not isinstance(body, bytes):
            body = json.dumps(body, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        u = urlparse(self.path)
        p = unquote(u.path)
        if p == "/api/meta":
            return self.send(200, meta())
        if p == "/api/companies":
            return self.send(200, query(parse_qs(u.query)))
        if p.startswith("/api/companies/"):
            c = BY_ID.get(p.rsplit("/", 1)[1])
            if not c:
                return self.send(404, {"error": "not found"})
            return self.send(200, {k: v for k, v in c.items() if k != "_idx"})
        f = (STATIC / ("index.html" if p == "/" else p.lstrip("/"))).resolve()
        if STATIC in f.parents and f.is_file():
            return self.send(200, f.read_bytes(), mimetypes.guess_type(f.name)[0] or "application/octet-stream")
        self.send(404, {"error": "not found"})

    def log_message(self, fmt, *a):
        sys.stderr.write("%s\n" % (fmt % a))

import io

def application(environ, start_response):
    class WSGIHandler(H):
        def __init__(self, req_env):
            self.environ = req_env
            self.path = req_env.get("PATH_INFO", "")
            qs = req_env.get("QUERY_STRING", "")
            if qs:
                self.path += "?" + qs
            self.command = req_env.get("REQUEST_METHOD", "GET")
            self.wfile = io.BytesIO()
            self.headers = {}
            self._headers_list = []
            self._status = "200 OK"

        def send_response(self, code, message=None):
            self._status = f"{code} OK" if code == 200 else f"{code} Error"

        def send_header(self, keyword, value):
            self._headers_list.append((keyword, str(value)))

        def end_headers(self):
            pass

    handler = WSGIHandler(environ)
    if environ.get("REQUEST_METHOD") == "GET":
        handler.do_GET()
    else:
        handler.send(405, {"error": "Method not allowed"})

    start_response(handler._status, handler._headers_list)
    return [handler.wfile.getvalue()]

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--port", type=int, default=8000)
    a = ap.parse_args()
    print(f"{len(DATA)} companies loaded; http://{a.host}:{a.port}")
    ThreadingHTTPServer((a.host, a.port), H).serve_forever()