# retoor <retoor@molodetz.nl>
from __future__ import annotations

import json
import logging
import math
import re
from dataclasses import dataclass, field
from pathlib import Path

logger = logging.getLogger(__name__)

README_SCAN_BYTES: int = 6144
SATURATION: float = 16.0
README_CAP: float = 18.0
CONSTELLATION_FREEBIES: int = 3
CONSTELLATION_STEP: float = 1.2
CONSTELLATION_CAP: float = 12.0

KNOWN_TEMPLATE_SLUGS: tuple[tuple[str, float], ...] = (
    ("next-js-boilerplate", 9.0),
    ("nextjs-boilerplate", 9.0),
    ("next-js-blog-boilerplate", 9.0),
    ("vercel-ai-chatbot", 9.0),
    ("ai-chatbot", 9.0),
    ("create-t3-app", 9.0),
    ("fullstack-nextjs-app-template", 9.0),
    ("my-v0-project", 9.0),
    ("nextjs-starter", 8.0),
    ("saas-starter", 8.0),
    ("astro-boilerplate", 8.0),
    ("vite-project", 7.0),
    ("my-t3-app", 9.0),
    ("my-app", 5.0),
    ("chatbot", 5.0),
)

KNOWN_TEMPLATE_AUTHORS: tuple[str, ...] = (
    "ixartz",
    "uiux lab",
    "shoubhit dash",
    "vercel",
    "shadcn",
    "steven tey",
)

GENERIC_IDENTITY_PATTERN = re.compile(r"\b(boilerplate|starter|template|scaffold)\b", re.IGNORECASE)

README_MARKERS: tuple[tuple[str, float, str], ...] = (
    (r"bootstrapped with .{0,40}create", 10.0, "bootstrapped-with"),
    (r"create[- ]t3[- ]app|\bt3 stack\b", 9.0, "t3-stack"),
    (r"(free,? )?open[- ]source (template|starter)|\b(app|chatbot|website) template\b|template built with", 9.0, "template-marketing"),
    (r"\bboilerplate\b", 8.0, "boilerplate-wording"),
    (r"\bstarter\b", 6.0, "starter-wording"),
    (r"getting started.{0,160}first,? run the development server", 7.0, "default-readme"),
    (r"deploy your own|one[- ]click deploy|deploy with vercel", 5.0, "deploy-your-own"),
    (r"https?://demo\.|/demo/", 4.0, "hosted-demo"),
    (r"\bsponsors?\b", 3.0, "sponsor-section"),
)

SCAFFOLD_ARTIFACTS: tuple[str, ...] = (
    ".storybook",
    ".husky",
    ".devcontainer",
    ".github/FUNDING.yml",
    ".github/dependabot.yml",
    ".github/workflows",
    "commitlint.config.js",
    "commitlint.config.ts",
    "lint-staged.config.js",
    ".lintstagedrc",
    ".lintstagedrc.js",
    "renovate.json",
    "playwright.config.js",
    "playwright.config.ts",
    "vitest.config.js",
    "vitest.config.ts",
    "drizzle.config.ts",
    "components.json",
    "crowdin.yml",
    ".releaserc",
    ".releaserc.json",
    "docker-compose.yml",
    "Dockerfile",
    "sentry.client.config.ts",
    "sentry.server.config.ts",
    "CHANGELOG.md",
    "CODE_OF_CONDUCT.md",
    "oxlint.config.ts",
)


@dataclass(frozen=True)
class TemplateEvidence:
    score: float = 0.0
    markers: list[str] = field(default_factory=list)


def _manifest_markers(root: Path) -> tuple[float, list[str]]:
    manifest = root / "package.json"
    if not manifest.exists():
        return 0.0, []
    try:
        body = json.loads(manifest.read_text(encoding="utf-8", errors="replace"))
    except (json.JSONDecodeError, OSError):
        return 0.0, []
    if not isinstance(body, dict):
        return 0.0, []
    weight = 0.0
    markers: list[str] = []
    name = str(body.get("name") or "").lower()
    matched_slug = next(
        ((slug, slug_weight) for slug, slug_weight in KNOWN_TEMPLATE_SLUGS if slug in name), None
    )
    if matched_slug:
        weight += matched_slug[1]
        markers.append(f"package name matches known template '{matched_slug[0]}'")
    identity = f"{name} {str(body.get('description') or '')}"
    if not matched_slug and GENERIC_IDENTITY_PATTERN.search(identity):
        weight += 7.0
        markers.append("package identity describes itself as a boilerplate/starter/template")
    author = str(body.get("author") or "").lower()
    if any(known in author for known in KNOWN_TEMPLATE_AUTHORS):
        weight += 6.0
        markers.append("package author is a known template publisher")
    if "ct3aMetadata" in body:
        weight += 10.0
        markers.append("create-t3-app scaffold metadata present")
    return weight, markers


def _readme_markers(root: Path) -> tuple[float, list[str]]:
    readme = next((root / name for name in ("README.md", "readme.md", "README") if (root / name).exists()), None)
    if readme is None:
        return 0.0, []
    try:
        text = readme.read_text(encoding="utf-8", errors="replace")[:README_SCAN_BYTES]
    except OSError:
        return 0.0, []
    weight = 0.0
    markers: list[str] = []
    for pattern, marker_weight, label in README_MARKERS:
        if re.search(pattern, text, re.IGNORECASE | re.DOTALL):
            weight += marker_weight
            markers.append(f"README carries template marker: {label}")
    return min(weight, README_CAP), markers


def _constellation_markers(root: Path) -> tuple[float, list[str]]:
    present = [artifact for artifact in SCAFFOLD_ARTIFACTS if (root / artifact).exists()]
    extra = max(0, len(present) - CONSTELLATION_FREEBIES)
    if extra <= 0:
        return 0.0, []
    weight = min(extra * CONSTELLATION_STEP, CONSTELLATION_CAP)
    return weight, [f"kitchen-sink scaffold constellation: {len(present)} standard scaffold artifacts"]


def detect_template(root: Path) -> TemplateEvidence:
    weight = 0.0
    markers: list[str] = []
    for probe in (_manifest_markers, _readme_markers, _constellation_markers):
        try:
            probe_weight, probe_markers = probe(root)
        except OSError as error:
            logger.warning("Template probe failed in %s: %s", root, error)
            continue
        weight += probe_weight
        markers.extend(probe_markers)
    score = round(100.0 * (1.0 - math.exp(-weight / SATURATION)), 1) if weight > 0 else 0.0
    return TemplateEvidence(score=score, markers=markers)
