"""The **technical config wizard** that produces ``Agent/config.json`` — the inline
question flow of ``alexandria install``, run by ``alexandria init`` when no ``--config`` is
given and stdin is a TTY. This is *not* the owner's personalization: that's the separate
``alexandria interview`` → the in-vault ``settings`` first-run pass ("Hello").

Captures the **technical** config for a working, *neutral* vault — the owner's **delivery
email** + digest routing (a conflict-free ``owner_digests`` partition + an optional team
roster), the chosen automation runner, enabled modules, the maintainer, and secret *names*.
It stays **identity-neutral**: the agent is the default (name **"Alexandria"**,
``personalized: false``, no character), there are **no areas**, and the owner's **display
name is not collected**. The **owner** supplies their name (→ their personal area, via
``alexandria seed-area``), names + personalizes the agent, and adds their real areas later —
in the **interview** (the ``settings`` first-run pass). This keeps the roles clean: the
technician **install** makes it *work*; the owner's **interview** makes it *theirs*.

Pure stdlib to import (``run_config_wizard`` derives the required secret *names* from the
enabled modules' manifests via ``scripts.modules`` at run time).
``run_config_wizard(ask=...)`` drives every prompt through one ``ask(prompt)->str``
callable, so it is fully scriptable (and testable) by passing a canned ``ask``; the default
reads from the terminal. It returns a config dict that passes ``config.validate`` — the
``owner_digests`` partition is built programmatically (one ``rest`` catch-all + one
list-digest per area) so it is conflict-free by construction.
"""
from __future__ import annotations

import re

from .config import detect_timezone as _detect_timezone


class WizardAborted(Exception):
    """The config wizard can't complete — EOF/Ctrl-C, or a non-interactive run that would
    still need to prompt. The CLI turns this into a clean message + non-zero exit."""


# --- input plumbing --------------------------------------------------------------------
def _interactive_ask(prompt: str) -> str:
    try:
        return input(prompt).strip()
    except (EOFError, KeyboardInterrupt):
        raise WizardAborted("setup cancelled")


def scripted_ask(answers):
    """An ``ask`` backed by a fixed list of answers — for --answers files and tests."""
    it = iter(answers)

    def ask(prompt: str) -> str:
        try:
            return str(next(it)).strip()
        except StopIteration:
            raise WizardAborted("ran out of scripted answers at prompt: " + prompt)
    return ask


def _text(ask, label, default="", required=False):
    hint = f" [{default}]" if default else ""
    while True:
        v = ask(f"{label}{hint}: ") or default
        if v or not required:
            return v
        print("  (required — please enter a value)")


def _yesno(ask, label, default=False):
    d = "Y/n" if default else "y/N"
    while True:
        v = ask(f"{label} [{d}]: ").lower()
        if not v:
            return default
        if v in ("y", "yes"):
            return True
        if v in ("n", "no"):
            return False
        print("  (please answer y or n)")


def _list(ask, label, default=None):
    suffix = f" [{', '.join(default)}]" if default else ""
    v = ask(f"{label} (comma-separated){suffix}: ")
    if not v:
        return list(default) if default else []
    return [x.strip() for x in v.split(",") if x.strip()]


def _int(ask, label, default):
    while True:
        v = ask(f"{label} [{default}]: ")
        if not v:
            return default
        try:
            return int(v)
        except ValueError:
            print("  (please enter a whole number)")


def _choice(ask, label, choices, default):
    while True:
        v = ask(f"{label} {list(choices)} [{default}]: ") or default
        if v in choices:
            return v
        print(f"  (pick one of {list(choices)})")


def _slug(s: str) -> str:
    return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") or "x"


def _plus_alias(email: str, name: str) -> str:
    """The standard agent mailbox: a ``+alias`` of the owner's own address."""
    tag = re.sub(r"[^a-z0-9]+", "", name.lower()) or "agent"
    if "@" in email and "+" not in email.split("@", 1)[0]:
        local, domain = email.split("@", 1)
        return f"{local}+{tag}@{domain}"
    return email


# --- sections --------------------------------------------------------------------------
def _owner_digests(ask, areas, primary_to):
    """One digest covering everything, to the owner. Areas can be split into
    their own digests later (config ``owner_digests``); the wizard keeps first
    setup to a single question."""
    return [{"id": "rest", "title": "Daily Digest", "areas": "rest",
             "to": primary_to, "personal": True}]


def _team(ask):
    team = []
    if not _yesno(ask, "\nAdd team members who should get their own briefs?", default=False):
        return team
    while True:
        name = _text(ask, "  Member name", required=True)
        role = _text(ask, "    Role")
        email = _text(ask, "    Email", required=True)
        scope_areas = _list(ask, "    Areas they help with")
        tz = _text(ask, "    Timezone", default=_detect_timezone())
        hour = _int(ask, "    Send hour (local)", 9)
        reply = _yesno(ask, "    Allow them to reply into the vault?", default=True)
        team.append({"id": _slug(name), "name": name, "role": role, "email": email,
                     "scope": {"areas": scope_areas, "delegated_to": name},
                     "timezone": tz, "send_hour_local": hour, "reply": reply})
        if not _yesno(ask, "  Add another member?", default=False):
            break
    return team


# --- the interview ---------------------------------------------------------------------
def run_config_wizard(ask=None) -> dict:
    """Conduct the technical config wizard and return a validated config dict. ``ask`` is a
    ``(prompt:str) -> str`` callable (defaults to reading the terminal)."""
    ask = ask or _interactive_ask
    print("\n— Alexandria install · technical setup —")
    print("Answer a handful of questions to stand up a working vault (writes Agent/config.json).\n")

    # The agent — the neutral default. The OWNER names + personalizes it later, in Claude
    # Code at the vault ("Hello" → the `settings` first-run pass); the technician install
    # just stands up a working, neutral vault. `personalized: false` marks it un-personalized
    # so the in-vault agent greets a first-time owner and offers to set things up (AGENTS.md).
    agent_name = "Alexandria"
    character = []  # personality is the owner's to set later — keep the Core baseline for now

    # The owner's DELIVERY details only — where the daily brief is sent. The owner's DISPLAY
    # name, their real areas, and the agent's name/personality are personalization the OWNER
    # sets at first-run ("Hello" → the settings pass); the technician install stays identity-neutral.
    tz = _detect_timezone()  # auto-detected from the host — no friction; override in config.json if wrong
    print(f"\nHome timezone (auto-detected): {tz}")
    emails = _list(ask, "Owner's email — where the daily digest is delivered")
    while not emails:
        print("  (at least one email is required)")
        emails = _list(ask, "Owner's email — where the daily digest is delivered")

    # The agent mailbox is a +alias of the owner's address. The neutral default uses
    # "alexandria"; the owner re-aliases it to the agent's real name when they name it.
    agent_email = _plus_alias(emails[0], agent_name)

    # No areas at install — the owner adds their real areas, and their personal area (via
    # `alexandria seed-area`), during personalization.
    areas = []

    primary_to = _text(ask, "Where should the daily digest go?", default=emails[0])
    owner_digests = _owner_digests(ask, areas, primary_to)

    team = _team(ask)

    print("")
    runner = _choice(ask, "Automation runner", ("laptop-github-actions", "server-macos"),
                     "laptop-github-actions")
    send_hour = _int(ask, "Daily digest send hour (local, 0-23)", 9)

    repo = _text(ask, "Vault's private GitHub repo (owner/name)", required=True)
    maint_name = _text(ask, "Alexandria maintainer — your name (the deployer)", required=True)
    maint_contact = _text(ask, "Maintainer contact (email)", required=True)

    enabled = ["core", runner]
    if team:
        enabled.append("team-digests")
    if _yesno(ask, "Enable attention-digest (on-demand \"what needs me\")?", default=True):
        enabled.append("attention-digest")
    if _yesno(ask, "Enable principal-inbox (owner emails their own vault)?", default=False):
        enabled.append("principal-inbox")

    backend = "github-actions" if runner == "laptop-github-actions" else "dotenv"
    # Secret NAMES come from the enabled modules' manifests (the single source of truth),
    # so each deployment gets exactly what it needs — e.g. server-macos requires IMAP_*
    # for its IDLE listener. (No software-repo token anywhere: the engine wheel is
    # vendored into the vault at .github/engine/ by the render.)
    from . import modules, __version__
    required = modules.required_secrets(enabled)

    config = {
        "alexandria": {"version": __version__, "template_ref": f"v{__version__}",
                       "source_repo": "keystone-computer/alexandria", "source_ref": "main",
                       "maintainer": {"name": maint_name, "contact": maint_contact}},
        "owner": {"timezone_default": tz, "emails": emails},
        "agent": {"name": agent_name, "character": character, "personalized": False,
                  "email": agent_email, "ingress_email": agent_email},
        "areas": areas,
        "owner_digests": owner_digests,
        "team": team,
        "automation": {"runner": runner, "send_hour_local": send_hour, "weather": True},
        "modules": {"enabled": enabled},
        "secrets": {"backend": backend, "required": required},
        "repo": repo,
    }
    if not character:
        config["agent"].pop("character")
    if not team:
        config.pop("team")

    # Validate with the real schema checker; surface problems, let the user bail on errors.
    from . import config as cfgmod
    problems = cfgmod.validate(config)
    errors = [p for p in problems if p.startswith("ERROR")]
    if problems:
        print("\nConfig check:")
        for p in problems:
            print("  " + p)
    if errors and not _yesno(ask, "\nThere are errors above. Keep this config anyway?", default=False):
        raise WizardAborted("config has validation errors; not saved")
    print(f'\n✓ Technical setup complete — a neutral vault (agent stays "Alexandria"; the owner '
          f'personalizes it later via the interview), modules: {", ".join(enabled)}.')
    return config
