import inspect
import re
import shutil
from pathlib import Path

import features
from controllers.base import word_names, word_parameters
from controllers.types import CONTROLLERS
from providers.base import LIBRARY
from engine.package import data
from engine.reach import Reach
from providers.skill_homes import LINKED, RETIRED, library, link, pruned, skill_name
from engine.frontmatter import FRONTMATTER, frontmatter

HERE = data()


def signature(controller: type, name: str) -> str:
    words = []
    for p in word_parameters(controller, name):
        if p.kind is inspect.Parameter.VAR_KEYWORD:
            words.append("[--set key=value…]")
        elif p.default is inspect.Parameter.empty:
            words.append(f"<{p.name}>")
        else:
            words.append(f"[--{p.name} …]" if not isinstance(p.default, bool) else f"[--{p.name}]")
    return " ".join(words)


NOUNS = "references/nouns.md"


def reference() -> str:
    named = {type_: word_names(controller) for type_, controller in CONTROLLERS.items()}
    shared = set.intersection(*named.values())
    out = ["## Reference: every noun and its own words", "",
           f"Every noun takes these words: {', '.join(sorted(shared))}. A noun that renames one says so below. "
           "journal <noun> --help prints every word with its arguments.", ""]
    owners = {type(f).__module__.rsplit(".", 1)[0]: name for name, f in features.FEATURES.items()}
    for type_, controller in CONTROLLERS.items():
        r = controller.resource
        owner = owners.get(controller.__module__.rsplit(".", 1)[0])
        out.append(f"### {type_} — {r.details.abstract}")
        taught = teaching(features.FEATURES[owner]) if owner else ""
        told = f"The {skill_name(taught)} skill says how it works." if taught else r.details.help
        out.append(f"{told}  Scope: {r.scope}. Seen by: {', '.join(r.notified) or 'nobody'}.")
        renamed = [f"{name} is {word}" for name, word in sorted(r.command_names.items()) if name in shared and word != name]
        if renamed:
            out.append(f"Renamed: {', '.join(renamed)}.")
        for name in sorted(named[type_] - shared):
            out.append(f"    journal {type_} {r.command_names.get(name, name)} {signature(controller, name)}".rstrip())
        out.append("")
    return "\n".join(out)


def core() -> str:
    text = (HERE / "skills" / "journal.md").read_text()
    return (f"---\nname: journal\ndescription: The journal, its commands and when each applies; load it before the first write\n---\n\n{text}\n"
            f"## Reference\n\nEvery noun and its own words are in {NOUNS}, beside this file: read it when you need a noun's exact words. "
            "journal <noun> --help prints every word with its arguments.\n")


def subject(name: str) -> str:
    source = HERE / "skills" / f"{name}.md"
    if not source.is_file():
        return ""
    body = re.sub(r"\A---\n.*?\n---\n", "", source.read_text(), count=1, flags=re.S).strip()
    return "\n".join(line for line in body.splitlines() if not line.startswith("# ")).strip()


def plain_description(text: str) -> str:
    return re.sub(r"\s*[<>]\s*", " ", text).strip()


def teaching(f) -> str:
    return f.details.skill_of or (f.name if f.details.has_skill else "")


def folded_into(name: str) -> list:
    return [f for f in features.FEATURES.values() if f.details.skill_of == name]


def listed(words) -> str:
    return ", ".join(dict.fromkeys(words))


def nouns(names) -> list[str]:
    return [name for name in names if "_" not in name]


def sentence(text: str) -> str:
    return text if text.endswith((".", "!", "?")) else f"{text}."


def reaching(f) -> str:
    told = [*(f"the line {name}" for name, line in f.lines.items() if line.reach != Reach.MAIN),
            *(f"the guard {guard.name}" for guard in f.journal.agent.guards if guard.reach != Reach.MAIN)]
    return f"These reach your subagents too: {', '.join(told)}." if told else "Its lines and guards are for the main agent only; none reach a subagent."


def folded_parts(folded: list) -> str:
    return "".join(f"\n## {f.title}\n\n{sentence(f.abstract)}\n\n{f.help}\n\n{reaching(f)}\n" for f in folded)


def moments(whens: list[str]) -> str:
    return "; or when ".join(when for when in whens if when)


def head(name: str, description: str, keywords: list[str], commands: list[str], extra: str = "") -> str:
    return (f"---\nname: {skill_name(name)}\n{extra}description: {plain_description(description)}\n"
            f"{'keywords: ' + listed(keywords) + chr(10) if keywords else ''}commands: {listed(commands)}\n---\n")


def feature_skill(f) -> str:
    folded = folded_into(f.name)
    subject_text = subject(f.name)
    when = moments([f.when, *(g.when for g in folded)])
    loaded = f"Load it when {when}. {sentence(f.abstract)}" if when else f"{sentence(f.abstract)} It runs by itself; load it to read how it works."
    return (head(f.name, loaded, [*f.keywords, *(w for g in folded for w in g.keywords)], nouns([f.name, *(g.name for g in folded)]))
            + f"\n# {f.title}\n\n{sentence(f.abstract)}\n\n{f.help}\n\n{reaching(f)}\n{chr(10) + subject_text + chr(10) if subject_text else ''}{folded_parts(folded)}")


def subject_skill(source: Path) -> str:
    folded = folded_into(source.stem)
    text = source.read_text()
    front = FRONTMATTER.match(text)
    fields = frontmatter(text)
    when = moments([g.when for g in folded])
    description = f"{fields.get('description', '')} Load it when {when}." if when else fields.get("description", "")
    keywords = [*(w.strip() for w in fields.get("keywords", "").split(",") if w.strip()), *(w for g in folded for w in g.keywords)]
    commands = nouns([source.stem, *(w.strip() for w in fields.get("commands", "").split(",") if w.strip()), *(g.name for g in folded)])
    extra = "".join(f"{key}: {value}\n" for key, value in fields.items() if key not in ("name", "description", "keywords", "commands"))
    return head(source.stem, description, keywords, commands, extra) + text[front.end():].rstrip("\n") + "\n" + folded_parts(folded)


def render() -> dict[str, str]:
    features.load()
    out = {"journal/SKILL.md": core(), f"journal/{NOUNS}": reference()}
    for source in sorted((HERE / "skills").glob("*.md")):
        if source.name != "journal.md" and source.stem not in features.FEATURES:
            out[f"{skill_name(source.stem)}/SKILL.md"] = subject_skill(source)
    for name, f in features.FEATURES.items():
        if f.details.has_skill and not f.details.skill_of:
            out[f"{skill_name(name)}/SKILL.md"] = feature_skill(f)
    return out


def write(folder: Path) -> list[Path]:
    written = []
    for path, text in render().items():
        f = folder / path
        if f.parent.is_symlink():
            f.parent.unlink()
        f.parent.mkdir(parents=True, exist_ok=True)
        if not f.is_file() or f.read_text() != text:
            f.write_text(text)
        written.append(f)
    return written


def publish(project: Path, agents: tuple[str, ...]) -> tuple[list[Path], list[Path]]:
    written = write(project / LIBRARY)
    names = sorted({f.relative_to(project / LIBRARY).parts[0] for f in written})
    pruned(project, names)
    for home in RETIRED:
        if library(project, project / home):
            continue
        for stale in (project / home).glob("journal*"):
            if stale.is_dir() and not stale.is_symlink():
                shutil.rmtree(stale)
    linked = link(project, names, tuple(a for a in agents if a in LINKED))
    return written, linked
