"""2real-team CLI — AI agent team framework for Claude Code projects."""

from __future__ import annotations

from pathlib import Path

import typer
from rich.console import Console
from rich.table import Table

from .bootstrap import bootstrap_project, generate_team, make_email
from .models import (
    InstallConfig,
    TeamConfig,
    YamlConfig,
    _read_yaml_mapping,
    is_unified_config,
)
from .presets import get_preset, list_presets

app = typer.Typer(name="2real-team", help="AI agent team framework for Claude Code projects")
console = Console()


@app.command()
def init(
    preset: str = typer.Option(
        None, help="Project preset (fullstack-monorepo, data-pipeline, library)"
    ),
    team_size: int = typer.Option(None, help="Override default team size"),
    config: str = typer.Option(
        None,
        help="Path to a YAML config file — either the unified install config "
        "(see `init --help` key reference) or the legacy flat team config",
    ),
    project_name: str = typer.Option(None, help="Project name"),
    target: str = typer.Option(".", help="Target directory"),
    interactive: bool = typer.Option(True, help="Interactive mode"),
    non_interactive: bool = typer.Option(
        False,
        "--non-interactive",
        help="Guarantee zero prompts; the config (flags > YAML > defaults) answers everything",
    ),
    git_email_prefix: str = typer.Option(
        "", help="Email prefix (e.g., 'myorg' -> myorg+First.Last@gmail.com)"
    ),
    ai_personas: bool = typer.Option(False, help="Use Claude API to generate rich personas"),
    seed: int = typer.Option(None, help="Seed for reproducible AI persona generation"),
    with_hooks: bool = typer.Option(
        True,
        "--with-hooks/--no-hooks",
        help="Also install the framework runtime (hooks/lib/skills/config + settings wiring)",
    ),
    owner: str = typer.Option(None, help="SCM org/user for the framework config (scm.owner)"),
    merge_model: str = typer.Option(
        None, help="Default merge model: wave-branch | direct-to-main"
    ),
    with_ontology: bool = typer.Option(
        None,
        "--with-ontology/--no-ontology",
        help=(
            "Seed the ontology semantic-overlay template and generate the structural index "
            "(ontology/structural/) at install time (requires --with-hooks). "
            "Unset, it follows the install config's ontology.enabled (default: on)"
        ),
    ),
) -> None:
    """Bootstrap a new project with the team framework.

    --config accepts either YAML schema (auto-detected):

    UNIFIED install config (shared with framework/install/bootstrap.py;
    precedence: CLI flags > YAML > shipped defaults):
    version (1) | repo.expect fresh|existing|any | project.name |
    project.model standalone|meta|child | scm.provider github | scm.owner |
    ci.provider github-actions | ticketing.provider github-issues |
    pre_push.mode noop|enforce|none | ontology.enabled bool |
    team.enabled bool | team.preset | team.size |
    children [{path, flavor: product|infra}]

    LEGACY flat team config: preset (required) | project_name | team_size |
    git_email_prefix | target | skills | members.

    With --non-interactive (or any --config) there are zero prompts.
    """
    target_path = Path(target).resolve()
    if non_interactive:
        interactive = False

    yaml_cfg: YamlConfig | None = None
    install_cfg: InstallConfig | None = None
    if config:
        try:
            raw = _read_yaml_mapping(config)
            if is_unified_config(raw):
                install_cfg = InstallConfig.from_yaml(config)
            else:
                yaml_cfg = YamlConfig.from_yaml(config)
        except (FileNotFoundError, ValueError) as exc:
            console.print(f"[red]Error:[/red] {exc}")
            raise typer.Exit(1) from exc

        # Config mode is fully non-interactive.
        interactive = False
        if install_cfg is not None:
            # Unified schema: CLI flags win over the YAML.
            preset = preset or install_cfg.team.preset
            team_size = team_size or install_cfg.team.size
            project_name = project_name or install_cfg.project.name
            owner = owner or install_cfg.scm.owner
        else:
            assert yaml_cfg is not None
            preset = yaml_cfg.preset
            team_size = yaml_cfg.team_size or team_size
            project_name = yaml_cfg.project_name or project_name
            git_email_prefix = yaml_cfg.git_email_prefix or git_email_prefix
            if yaml_cfg.target != ".":
                target_path = Path(yaml_cfg.target).resolve()

    team_enabled = install_cfg.team.enabled if install_cfg is not None else True
    if not team_enabled:
        _install_runtime_only(
            target_path, with_hooks, owner, merge_model, config, with_ontology
        )
        return

    if not preset and interactive:
        available = list_presets()
        console.print("\n[bold]Available presets:[/bold]")
        for p in available:
            console.print(
                f"  [cyan]{p.name}[/cyan] — {p.description}"
                f" (default size: {p.default_team_size})"
            )
        preset = typer.prompt("\nChoose a preset", type=str)

    if not preset:
        console.print("[red]Error:[/red] --preset is required in non-interactive mode")
        raise typer.Exit(1)

    if not project_name and interactive:
        project_name = typer.prompt("Project name", default=target_path.name)
    elif not project_name:
        project_name = target_path.name

    preset_config = get_preset(preset)

    if not team_size and interactive:
        team_size = typer.prompt(
            "Team size",
            default=preset_config.default_team_size,
            type=int,
        )

    console.print(f"\n[bold]Generating team for [cyan]{project_name}[/cyan]...[/bold]")
    # Determine skills list — config overrides preset defaults
    skills = preset_config.skills
    if yaml_cfg is not None and yaml_cfg.skills is not None:
        skills = yaml_cfg.skills

    members = generate_team(preset_config, team_size)

    # Apply per-member overrides from YAML config (legacy schema only)
    if yaml_cfg is not None and yaml_cfg.members:
        for i, override in enumerate(yaml_cfg.members):
            if i >= len(members):
                break
            if override.name:
                members[i].name = override.name
                parts = override.name.split(" ", 1)
                first = parts[0]
                last = parts[1] if len(parts) > 1 else ""
                members[i].email = make_email(first, last, git_email_prefix)
            if override.role:
                members[i].role = override.role
            if override.level:
                members[i].level = override.level
            if override.personality:
                members[i].personality = override.personality

    # Apply AI-generated personas if requested
    if ai_personas:
        from .personas import generate_personas

        roles = [{"role": m.role, "level": m.level} for m in members]
        personas = generate_personas(preset_config, roles, len(members), seed)
        if personas:
            for i, persona in enumerate(personas):
                if i >= len(members):
                    break
                members[i].name = persona["name"]
                parts = persona["name"].split(" ", 1)
                first = parts[0]
                last = parts[1] if len(parts) > 1 else ""
                members[i].email = make_email(first, last, git_email_prefix)
                members[i].personality = persona["personality"]
            console.print("[dim]AI personas generated successfully[/dim]")
        else:
            console.print("[dim]Using local name pool (AI generation unavailable)[/dim]")

    team_config = TeamConfig(
        project_name=project_name,
        preset=preset,
        team_members=members,
        skills=skills,
        git_email_prefix=git_email_prefix,
    )

    created = bootstrap_project(target_path, team_config)

    console.print(f"\n[green]Created {len(created)} files:[/green]")
    for f in created:
        console.print(f"  {f}")

    # Install the config-driven framework runtime (hooks/lib/skills/config +
    # dispatcher wiring) on top of the mustache team scaffolding.
    if with_hooks:
        _run_framework_install(
            target_path,
            owner=owner or git_email_prefix or None,
            merge_model=merge_model,
            install_config=config if install_cfg is not None else None,
            with_ontology=with_ontology,
        )
        # The bridge passes --no-team (this CLI scaffolded the roster itself),
        # so correct the recorded install snapshot to reflect the real outcome.
        _sync_install_snapshot(target_path, preset=preset, team_size=len(members))

    console.print(f"\n[bold green]Team framework bootstrapped for '{project_name}'![/bold green]")
    console.print("Next steps:")
    console.print("  1. Review .claude/team/charter.md")
    console.print("  2. Customize roster cards in .claude/team/roster/")
    console.print("  3. Review root CLAUDE.md (any prior CLAUDE.md was kept as .bak)")
    if with_hooks:
        console.print(
            "  4. Review .claude/framework.config.json + restart Claude Code so hooks load."
        )


def _run_framework_install(
    target_path: Path,
    *,
    owner: str | None,
    merge_model: str | None,
    install_config: str | None,
    with_ontology: bool | None = None,
) -> None:
    """Invoke the framework bootstrapper and report the outcome."""
    from .framework_install import install_framework

    proc = install_framework(
        target_path,
        owner=owner,
        merge_model=merge_model,
        install_config=install_config,
        with_ontology=with_ontology,
    )
    if proc is None:
        console.print(
            "\n[yellow]Framework runtime not installed:[/yellow] bundled assets not found "
            "(re-run from a source checkout, or use the standalone framework installer)."
        )
    elif proc.returncode != 0:
        console.print(
            "\n[yellow]Framework runtime install reported an issue "
            f"(exit {proc.returncode}):[/yellow]"
        )
        console.print(f"[dim]{(proc.stderr or proc.stdout).strip()[:500]}[/dim]")
    else:
        console.print(
            "\n[green]Installed the framework runtime[/green] (hooks/lib/skills/config)."
        )


def _sync_install_snapshot(target_path: Path, *, preset: str | None, team_size: int) -> None:
    """Record the team the CLI actually scaffolded in .claude/install.config.json."""
    import json

    snapshot_path = target_path / ".claude" / "install.config.json"
    if not snapshot_path.is_file():
        return
    try:
        snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return
    team = snapshot.setdefault("team", {})
    team["enabled"] = True
    team["preset"] = preset
    team["size"] = team_size
    snapshot_path.write_text(json.dumps(snapshot, indent=2) + "\n", encoding="utf-8")


def _install_runtime_only(
    target_path: Path,
    with_hooks: bool,
    owner: str | None,
    merge_model: str | None,
    config: str | None,
    with_ontology: bool | None = None,
) -> None:
    """Config said ``team.enabled: false`` — skip team scaffolding entirely."""
    console.print("[dim]team.enabled is false — skipping team scaffolding.[/dim]")
    if with_hooks:
        _run_framework_install(
            target_path,
            owner=owner,
            merge_model=merge_model,
            install_config=config,
            with_ontology=with_ontology,
        )
        console.print("\n[bold green]Framework runtime installed (no team layer).[/bold green]")
    else:
        console.print("[yellow]Nothing to do:[/yellow] team disabled and --no-hooks given.")


@app.command()
def add_member(
    name: str = typer.Argument(None, help="Member name (random if omitted)"),
    role: str = typer.Option(..., help="Role (e.g., 'Software Engineer')"),
    level: str = typer.Option("Senior", help="Level"),
    target: str = typer.Option(".", help="Target directory"),
) -> None:
    """Add a team member to the roster."""
    from .bootstrap import generate_name, make_email
    from .templates import render_template

    target_path = Path(target).resolve()
    roster_dir = target_path / ".claude" / "team" / "roster"

    if not roster_dir.exists():
        console.print("[red]Error:[/red] No roster directory found. Run `2real-team init` first.")
        raise typer.Exit(1)

    if not name:
        used = {p.stem.replace("_", " ") for p in roster_dir.glob("*.md")}
        first, last = generate_name(used)
        name = f"{first} {last}"

    parts = name.split(" ", 1)
    first, last = parts[0], parts[1] if len(parts) > 1 else ""
    email = make_email(first, last)

    context = {
        "name": name,
        "role": role,
        "level": level,
        "email": email,
        "personality": "To be defined.",
    }

    card = render_template("roster-card.md.mustache", context)
    safe_name = name.lower().replace(" ", "_").replace("-", "_")
    role_prefix = role.lower().replace(" ", "_")
    card_path = roster_dir / f"{role_prefix}_{safe_name}.md"
    card_path.write_text(card)

    rel = card_path.relative_to(target_path)
    console.print(f"[green]Added:[/green] {name} ({role}, {level}) -> {rel}")


@app.command()
def remove_member(
    name: str = typer.Argument(..., help="Member name to archive"),
    target: str = typer.Option(".", help="Target directory"),
) -> None:
    """Archive a team member (prefix with _departed_)."""
    target_path = Path(target).resolve()
    roster_dir = target_path / ".claude" / "team" / "roster"

    if not roster_dir.exists():
        console.print("[red]Error:[/red] No roster directory found.")
        raise typer.Exit(1)

    # Find the matching file
    safe_name = name.lower().replace(" ", "_").replace("-", "_")
    matches = list(roster_dir.glob(f"*{safe_name}*"))
    matches = [m for m in matches if not m.name.startswith("_departed_")]

    if not matches:
        console.print(f"[red]Error:[/red] No active roster card found for '{name}'")
        raise typer.Exit(1)

    for match in matches:
        archived = match.parent / f"_departed_{match.name}"
        match.rename(archived)
        console.print(f"[yellow]Archived:[/yellow] {match.name} → {archived.name}")


@app.command()
def update_member(
    name: str = typer.Argument(..., help="Member name to update"),
    role: str = typer.Option(None, help="New role"),
    level: str = typer.Option(None, help="New level"),
    target: str = typer.Option(".", help="Target directory"),
) -> None:
    """Update a team member's configuration."""
    target_path = Path(target).resolve()
    roster_dir = target_path / ".claude" / "team" / "roster"

    safe_name = name.lower().replace(" ", "_").replace("-", "_")
    matches = list(roster_dir.glob(f"*{safe_name}*"))
    matches = [m for m in matches if not m.name.startswith("_departed_")]

    if not matches:
        console.print(f"[red]Error:[/red] No active roster card found for '{name}'")
        raise typer.Exit(1)

    card_path = matches[0]
    content = card_path.read_text()

    if role:
        content = _replace_field(content, "Role", role)
    if level:
        content = _replace_field(content, "Level", level)

    card_path.write_text(content)
    console.print(f"[green]Updated:[/green] {name}")


@app.command()
def randomize_member(
    name: str = typer.Argument(..., help="Member name to regenerate"),
    target: str = typer.Option(".", help="Target directory"),
) -> None:
    """Regenerate a team member's name, background, and personality."""
    import random

    from .bootstrap import COMMUNICATION_STYLES, generate_name, make_email
    from .templates import render_template

    target_path = Path(target).resolve()
    roster_dir = target_path / ".claude" / "team" / "roster"

    safe_name = name.lower().replace(" ", "_").replace("-", "_")
    matches = list(roster_dir.glob(f"*{safe_name}*"))
    matches = [m for m in matches if not m.name.startswith("_departed_")]

    if not matches:
        console.print(f"[red]Error:[/red] No active roster card found for '{name}'")
        raise typer.Exit(1)

    old_path = matches[0]
    content = old_path.read_text()

    # Extract role and level from existing card
    role = _extract_field(content, "Role") or "Software Engineer"
    level = _extract_field(content, "Level") or "Senior"

    # Archive old
    archived = old_path.parent / f"_departed_{old_path.name}"
    old_path.rename(archived)

    # Generate new identity
    used = {p.stem.replace("_", " ") for p in roster_dir.glob("*.md")}
    first, last = generate_name(used)
    new_name = f"{first} {last}"
    email = make_email(first, last)

    context = {
        "name": new_name,
        "role": role,
        "level": level,
        "email": email,
        "personality": random.choice(COMMUNICATION_STYLES),
    }

    card = render_template("roster-card.md.mustache", context)
    new_safe = new_name.lower().replace(" ", "_").replace("-", "_")
    role_prefix = role.lower().replace(" ", "_")
    new_path = roster_dir / f"{role_prefix}_{new_safe}.md"
    new_path.write_text(card)

    console.print(f"[yellow]Archived:[/yellow] {name} → {archived.name}")
    console.print(f"[green]Created:[/green] {new_name} ({role}, {level}) → {new_path.name}")


@app.command()
def validate(
    target: str = typer.Option(".", help="Target directory"),
) -> None:
    """Verify charter, roster, and skills consistency."""
    target_path = Path(target).resolve()
    team_dir = target_path / ".claude" / "team"
    skills_dir = target_path / ".claude" / "skills"
    errors: list[str] = []

    if not team_dir.exists():
        console.print(
            "[red]Error:[/red] No .claude/team/ directory found."
            " Run `2real-team init` first."
        )
        raise typer.Exit(1)

    # Check charter exists
    if not (team_dir / "charter.md").exists():
        errors.append("Missing charter.md")

    # Check roster
    roster_dir = team_dir / "roster"
    if not roster_dir.exists():
        errors.append("Missing roster/ directory")
    else:
        active = [p for p in roster_dir.glob("*.md") if not p.name.startswith("_departed_")]
        if not active:
            errors.append("No active roster cards found")
        else:
            console.print(f"[dim]Found {len(active)} active team members[/dim]")

    # Check trust matrix
    if not (team_dir / "trust_matrix.md").exists():
        errors.append("Missing trust_matrix.md")

    # Check feedback log
    if not (team_dir / "feedback_log.md").exists():
        errors.append("Missing feedback_log.md")

    # Check skills
    if skills_dir.exists():
        skills = list(skills_dir.glob("*.md"))
        console.print(f"[dim]Found {len(skills)} skills[/dim]")
    else:
        console.print("[dim]No skills directory found[/dim]")

    if errors:
        console.print(f"\n[red]Validation failed with {len(errors)} error(s):[/red]")
        for e in errors:
            console.print(f"  [red]x[/red] {e}")
        raise typer.Exit(1)
    else:
        console.print("\n[green]Validation passed![/green]")


@app.command()
def status(
    target: str = typer.Option(".", help="Target directory"),
) -> None:
    """Show team composition and project status."""
    target_path = Path(target).resolve()
    roster_dir = target_path / ".claude" / "team" / "roster"

    if not roster_dir.exists():
        console.print("[red]No team found.[/red] Run `2real-team init` first.")
        raise typer.Exit(1)

    table = Table(title="Team Roster")
    table.add_column("Name", style="cyan")
    table.add_column("Role")
    table.add_column("Level")
    table.add_column("Status")

    for card_path in sorted(roster_dir.glob("*.md")):
        content = card_path.read_text()
        name = _extract_field(content, "Name") or card_path.stem
        role = _extract_field(content, "Role") or "?"
        level = _extract_field(content, "Level") or "?"
        is_departed = card_path.name.startswith("_departed_")
        status_str = "[red]Archived[/red]" if is_departed else "[green]Active[/green]"
        table.add_row(name, role, level, status_str)

    console.print(table)

    # Count skills
    skills_dir = target_path / ".claude" / "skills"
    if skills_dir.exists():
        skill_count = len(list(skills_dir.glob("*.md")))
        console.print(f"\nSkills installed: {skill_count}")


@app.command()
def uninstall(
    target: str = typer.Option(".", help="Target directory"),
    dry_run: bool = typer.Option(
        False, "--dry-run", help="Preview what would be removed; write nothing"
    ),
    non_interactive: bool = typer.Option(
        False,
        "--non-interactive",
        help="Never prompt; proceed without the confirmation gate (automation)",
    ),
) -> None:
    """Uninstall the framework runtime — restore the repo to its pre-install state.

    Removes exactly the framework-owned install set (hooks/lib/skills/config, charter, team
    artifacts, generated ontology, the managed .gitignore block, the pre-push hook) and restores
    any pre-install assets a consented install archived. Idempotent; a non-installed repo is a
    clean no-op.
    """
    from .framework_install import uninstall_framework

    target_path = Path(target).resolve()
    proc = uninstall_framework(
        target_path, dry_run=dry_run, non_interactive=non_interactive
    )
    if proc is None:
        console.print(
            "[yellow]Cannot uninstall:[/yellow] bundled framework assets not found "
            "(re-run from a source checkout, or use the standalone framework uninstaller)."
        )
        raise typer.Exit(1)
    if proc.stdout:
        console.print(proc.stdout.rstrip())
    if proc.returncode != 0:
        if proc.stderr:
            console.print(f"[red]{proc.stderr.strip()}[/red]")
        raise typer.Exit(proc.returncode)
    console.print("\n[bold green]Framework runtime uninstalled.[/bold green]")


def _extract_field(content: str, field: str) -> str | None:
    """Extract a field value from a roster card."""
    for line in content.splitlines():
        if f"**{field}:**" in line:
            return line.split(f"**{field}:**", 1)[1].strip()
    return None


def _replace_field(content: str, field: str, value: str) -> str:
    """Replace a field value in a roster card."""
    lines = content.splitlines()
    for i, line in enumerate(lines):
        if f"**{field}:**" in line:
            prefix = line.split(f"**{field}:**")[0]
            lines[i] = f"{prefix}**{field}:** {value}"
            break
    return "\n".join(lines)


if __name__ == "__main__":
    app()
