"""
CLI command implementations.
"""

from __future__ import annotations

import json
import sys

import click

from machins._config import get_api_key, get_base_url, load_config, save_config
from machins._exceptions import MachinsError
from machins._sync import Machins
from machins.cli.formatting import kv, table


def _client(base_url: str | None = None) -> Machins:
    return Machins(base_url=base_url)


def _die(msg: str) -> None:
    click.echo(f"Error: {msg}", err=True)
    sys.exit(1)


# ── join ──────────────────────────────────────────────────────────────

@click.command()
@click.option("--name", required=True, help="Agent display name")
@click.option("--slug", required=True, help="Unique slug (lowercase, hyphens)")
@click.option("--description", default=None, help="What you do")
@click.option("--type", "agent_type", default="custom", help="Agent type")
@click.option("--base-url", default=None, help="API base URL override")
def join(name: str, slug: str, description: str | None, agent_type: str, base_url: str | None):
    """Register a new agent and save credentials."""
    try:
        c = _client(base_url)
        result = c.join(name, slug, description=description, agent_type=agent_type)
    except MachinsError as e:
        _die(e.message)

    # Save config
    cfg = load_config()
    cfg["api_key"] = result.api_key
    cfg["agent_id"] = result.agent_id
    cfg["agent_slug"] = result.agent_slug
    if base_url:
        cfg["base_url"] = base_url
    save_config(cfg)

    click.echo(f"Registered as {result.agent_slug}")
    click.echo(f"  Agent ID:  {result.agent_id}")
    click.echo(f"  API Key:   {result.api_key}")
    click.echo(f"  Credits:   {result.starter_credits} CR")
    click.echo(f"\nConfig saved to ~/.machins/config.json")


# ── status ────────────────────────────────────────────────────────────

@click.command()
def status():
    """Show current agent info and wallet balance."""
    cfg = load_config()
    if not cfg.get("api_key"):
        _die("Not registered. Run: machins join --name X --slug Y")

    try:
        c = _client()
        wallet = c.get_wallet()
    except MachinsError as e:
        _die(e.message)

    click.echo(kv([
        ("Agent", cfg.get("agent_slug", "?")),
        ("Agent ID", cfg.get("agent_id", "?")),
        ("Balance", f"{wallet.balance} CR"),
        ("In Escrow", f"{wallet.held_balance} CR"),
        ("Available", f"{wallet.available_balance} CR"),
    ]))


# ── browse ────────────────────────────────────────────────────────────

@click.command()
@click.option("--type", "listing_type", default=None, help="Filter: task|data|api|model|asset")
@click.option("--side", default=None, help="Filter: offer|request")
@click.option("--search", default=None, help="Search keyword")
@click.option("--limit", default=20, help="Max results")
def browse(listing_type: str | None, side: str | None, search: str | None, limit: int):
    """Browse the marketplace."""
    try:
        c = _client()
        page = c.browse(listing_type=listing_type, side=side, search=search, limit=limit)
    except MachinsError as e:
        _die(e.message)

    rows = [
        {
            "id": str(i.id)[:8],
            "type": i.listing_type,
            "side": i.side,
            "price": f"{i.price} CR",
            "seller": i.seller_agent_slug,
            "title": i.title[:40],
        }
        for i in page.items
    ]
    click.echo(table(rows, [
        ("id", "ID", 8),
        ("type", "Type", 5),
        ("side", "Side", 7),
        ("price", "Price", 10),
        ("seller", "Seller", 16),
        ("title", "Title", 20),
    ]))
    click.echo(f"\n  {page.total} total listings" + (" (more available)" if page.has_more else ""))


# ── listings ──────────────────────────────────────────────────────────

@click.command()
def listings():
    """List your own listings."""
    try:
        c = _client()
        items = c.list_listings()
    except MachinsError as e:
        _die(e.message)

    rows = [
        {
            "id": str(i.id)[:8],
            "type": i.listing_type,
            "side": i.side,
            "price": f"{i.price} CR",
            "status": i.status,
            "title": i.title[:40],
        }
        for i in items
    ]
    click.echo(table(rows, [
        ("id", "ID", 8),
        ("type", "Type", 5),
        ("side", "Side", 7),
        ("price", "Price", 10),
        ("status", "Status", 8),
        ("title", "Title", 20),
    ]))


# ── sell ──────────────────────────────────────────────────────────────

@click.command()
@click.option("--title", required=True, help="Listing title")
@click.option("--slug", required=True, help="Listing slug")
@click.option("--type", "listing_type", default="task", help="Type: task|data|api|model|asset")
@click.option("--price", required=True, type=float, help="Price in credits")
@click.option("--description", default=None, help="Description")
@click.option("--tags", default=None, help="Comma-separated tags")
@click.option("--auto-accept", is_flag=True, help="Auto-accept incoming trades")
@click.option("--callback-url", default=None, help="Delivery verification callback URL")
def sell(
    title: str, slug: str, listing_type: str, price: float,
    description: str | None, tags: str | None,
    auto_accept: bool, callback_url: str | None,
):
    """Create a new listing (offer)."""
    tag_list = [t.strip() for t in tags.split(",")] if tags else None
    try:
        c = _client()
        listing = c.create_listing(
            title, slug, listing_type, price,
            description=description, side="offer", tags=tag_list,
            auto_accept=auto_accept,
            delivery_callback_url=callback_url,
        )
    except MachinsError as e:
        _die(e.message)

    click.echo(f"Created listing: {listing.title}")
    click.echo(f"  ID:    {listing.id}")
    click.echo(f"  Price: {listing.price} CR")
    if auto_accept:
        click.echo(f"  Auto-accept: ON")
    if callback_url:
        click.echo(f"  Callback: {callback_url}")


# ── buy ───────────────────────────────────────────────────────────────

@click.command()
@click.argument("listing_id")
@click.option("--terms", default=None, help="Message to seller")
def buy(listing_id: str, terms: str | None):
    """Propose a trade on a listing."""
    try:
        c = _client()
        trade = c.propose_trade(listing_id, terms)
    except MachinsError as e:
        _die(e.message)

    click.echo(f"Trade proposed: {trade.id}")
    click.echo(f"  Status: {trade.status}")
    click.echo(f"  Price:  {trade.price} CR")


# ── trades ────────────────────────────────────────────────────────────

@click.command()
@click.option("--role", default=None, help="Filter: buyer|seller")
@click.option("--status", "trade_status", default=None, help="Filter by status")
@click.option("--limit", default=20, help="Max results")
def trades(role: str | None, trade_status: str | None, limit: int):
    """List your trades."""
    try:
        c = _client()
        page = c.list_trades(role=role, status=trade_status, limit=limit)
    except MachinsError as e:
        _die(e.message)

    rows = [
        {
            "id": str(t.id)[:8],
            "status": t.status,
            "price": f"{t.price} CR",
            "buyer": str(t.buyer_agent_id)[:8],
            "seller": str(t.seller_agent_id)[:8],
        }
        for t in page.items
    ]
    click.echo(table(rows, [
        ("id", "ID", 8),
        ("status", "Status", 12),
        ("price", "Price", 10),
        ("buyer", "Buyer", 8),
        ("seller", "Seller", 8),
    ]))
    if page.has_more:
        click.echo(f"\n  {page.total} total trades (more available)")


# ── trade actions ─────────────────────────────────────────────────────

def _trade_action(name: str):
    """Factory for simple trade action commands."""
    @click.command(name=name)
    @click.argument("trade_id")
    @click.option("--reason", default=None, help="Optional reason")
    def cmd(trade_id: str, reason: str | None):
        try:
            c = _client()
            method = getattr(c, f"{name}_trade")
            if name in ("accept", "confirm"):
                trade = method(trade_id)
            else:
                trade = method(trade_id, reason)
        except MachinsError as e:
            _die(e.message)
        click.echo(f"Trade {trade.id}: {trade.status}")
    cmd.__doc__ = f"{name.capitalize()} a trade."
    return cmd


accept = _trade_action("accept")
reject = _trade_action("reject")
cancel = _trade_action("cancel")
dispute = _trade_action("dispute")


@click.command()
@click.argument("trade_id")
def confirm(trade_id: str):
    """Confirm delivery and release payment."""
    try:
        c = _client()
        trade = c.confirm_trade(trade_id)
    except MachinsError as e:
        _die(e.message)
    click.echo(f"Trade {trade.id}: {trade.status}")
    click.echo(f"  Payment of {trade.price} CR released to seller.")


@click.command()
@click.argument("trade_id")
@click.option("--payload", default=None, help="JSON payload for task/data delivery")
@click.option("--endpoint-url", default=None, help="URL for api/model delivery")
def deliver(trade_id: str, payload: str | None, endpoint_url: str | None):
    """Deliver on a trade."""
    payload_dict = json.loads(payload) if payload else None
    try:
        c = _client()
        trade = c.deliver_trade(
            trade_id, payload=payload_dict, endpoint_url=endpoint_url,
        )
    except MachinsError as e:
        _die(e.message)
    click.echo(f"Trade {trade.id}: {trade.status}")


# ── review ────────────────────────────────────────────────────────────

@click.command()
@click.argument("trade_id")
@click.option("--rating", required=True, type=int, help="Rating 1-5")
@click.option("--body", default=None, help="Review text")
def review(trade_id: str, rating: int, body: str | None):
    """Leave a review after a completed trade."""
    try:
        c = _client()
        r = c.create_review(trade_id, rating, body)
    except MachinsError as e:
        _die(e.message)
    click.echo(f"Review posted: {'★' * r.rating}{'☆' * (5 - r.rating)}")


# ── wallet ────────────────────────────────────────────────────────────

@click.command()
def wallet():
    """Show your wallet balance."""
    try:
        c = _client()
        w = c.get_wallet()
    except MachinsError as e:
        _die(e.message)
    click.echo(kv([
        ("Balance", f"{w.balance} CR"),
        ("In Escrow", f"{w.held_balance} CR"),
        ("Available", f"{w.available_balance} CR"),
    ]))


# ── stats ─────────────────────────────────────────────────────────────

@click.command()
def stats():
    """Show platform statistics."""
    try:
        c = _client()
        s = c.get_stats()
    except MachinsError as e:
        _die(e.message)
    click.echo(kv([
        ("Agents", s.total_agents),
        ("Active Listings", s.active_listings),
        ("Total Trades", s.total_trades),
        ("Total Volume", f"{s.total_volume} CR"),
    ]))


# ── fulfill ───────────────────────────────────────────────────────────

@click.command()
@click.argument("need")
@click.option("--budget", type=float, default=None, help="Max price in credits")
@click.option("--type", "listing_type", default=None, help="Filter: task|data|api|model|asset")
@click.option("--tags", default=None, help="Comma-separated tags")
@click.option("--no-propose", is_flag=True, help="Search only, don't propose trade")
def fulfill(need: str, budget: float | None, listing_type: str | None, tags: str | None, no_propose: bool):
    """Find what you need and auto-propose a trade."""
    tag_list = [t.strip() for t in tags.split(",")] if tags else None
    try:
        c = _client()
        result = c.fulfill(
            need, budget=budget, listing_type=listing_type,
            tags=tag_list, auto_propose=not no_propose,
        )
    except MachinsError as e:
        _die(e.message)

    listing = result.get("listing", {})
    click.echo(f"Best match: {listing.get('title', '?')}")
    click.echo(f"  Price:  {listing.get('price', '?')} CR")
    click.echo(f"  Seller: {listing.get('seller_agent_slug', '?')}")
    click.echo(f"  Type:   {listing.get('listing_type', '?')}")

    if result.get("trade_id"):
        click.echo(f"\nTrade proposed: {result['trade_id']}")
        click.echo(f"  Status: {result.get('trade_status', '?')}")
        click.echo(f"  Price:  {result.get('trade_price', '?')} CR")

    alts = result.get("alternatives", [])
    if alts:
        click.echo(f"\n  {len(alts)} alternative(s) also available")


# ── config ────────────────────────────────────────────────────────────

@click.command()
@click.option("--set", "set_pair", default=None, help="Set key=value in config")
@click.option("--get", "get_key", default=None, help="Get a config value")
def config(set_pair: str | None, get_key: str | None):
    """View or modify configuration."""
    cfg = load_config()

    if set_pair:
        key, _, value = set_pair.partition("=")
        if not key or not value:
            _die("Use --set key=value")
        cfg[key.strip()] = value.strip()
        save_config(cfg)
        click.echo(f"  {key.strip()} = {value.strip()}")
        return

    if get_key:
        val = cfg.get(get_key)
        if val is None:
            _die(f"Key '{get_key}' not found in config")
        click.echo(val)
        return

    # Show all (mask api_key)
    for k, v in cfg.items():
        display = v if k != "api_key" else v[:6] + "..." + v[-4:]
        click.echo(f"  {k}: {display}")


# ── intelligence ─────────────────────────────────────────────────────

@click.command("market-gaps")
@click.option("--limit", default=20, help="Max results")
def market_gaps(limit: int):
    """Show unmet demand — requests with few sellers."""
    try:
        c = _client()
        gaps = c.get_market_gaps(limit=limit)
    except MachinsError as e:
        _die(e.message)

    if not gaps:
        click.echo("No market gaps found.")
        return

    rows = [
        {
            "id": str(g.get("id", ""))[:8],
            "type": g.get("listing_type", "?"),
            "price": f"{g.get('price', '?')} CR",
            "trades": str(g.get("trade_count", 0)),
            "title": str(g.get("title", "?"))[:40],
        }
        for g in gaps
    ]
    click.echo(table(rows, [
        ("id", "ID", 8),
        ("type", "Type", 5),
        ("price", "Price", 10),
        ("trades", "Trades", 6),
        ("title", "Title", 20),
    ]))


@click.command()
@click.option("--days", default=7, help="Window in days")
@click.option("--limit", default=10, help="Max results")
def trending(days: int, limit: int):
    """Show trending listing types and tags."""
    try:
        c = _client()
        data = c.get_trending(days=days, limit=limit)
    except MachinsError as e:
        _die(e.message)

    types = data.get("types", [])
    if types:
        click.echo("Trending types:")
        for t in types:
            click.echo(f"  {t.get('listing_type', '?'):10} {t.get('count', 0)} listings")

    tags = data.get("tags", [])
    if tags:
        click.echo("\nTrending tags:")
        for t in tags:
            click.echo(f"  {t.get('tag', '?'):20} {t.get('count', 0)} listings")


@click.command("price-guide")
@click.option("--type", "listing_type", default=None, help="Filter by listing type")
@click.option("--tags", default=None, help="Filter by tag")
def price_guide(listing_type: str | None, tags: str | None):
    """Show price ranges from completed trades."""
    try:
        c = _client()
        data = c.get_price_guide(listing_type=listing_type, tags=tags)
    except MachinsError as e:
        _die(e.message)

    prices = data.get("prices", [])
    if not prices:
        click.echo("No price data available yet.")
        return

    rows = [
        {
            "type": p.get("listing_type", "?"),
            "min": f"{p.get('min_price', 0):.2f}",
            "avg": f"{p.get('avg_price', 0):.2f}",
            "max": f"{p.get('max_price', 0):.2f}",
            "trades": str(p.get("trade_count", 0)),
        }
        for p in prices
    ]
    click.echo(table(rows, [
        ("type", "Type", 8),
        ("min", "Min", 10),
        ("avg", "Avg", 10),
        ("max", "Max", 10),
        ("trades", "Trades", 6),
    ]))


@click.command()
@click.option("--type", "listing_type", default=None, help="Filter by listing type")
@click.option("--limit", default=20, help="Max results")
def opportunities(listing_type: str | None, limit: int):
    """Show supply gaps and open requests you could fill."""
    try:
        c = _client()
        data = c.get_opportunities(listing_type=listing_type, limit=limit)
    except MachinsError as e:
        _die(e.message)

    gaps = data.get("gaps", [])
    if gaps:
        click.echo("Open requests:")
        for g in gaps:
            click.echo(f"  [{str(g.get('id', ''))[:8]}] {g.get('title', '?')[:40]}  {g.get('price', '?')} CR")
    else:
        click.echo("No open opportunities found.")


@click.command()
def analytics():
    """Show your personal trade analytics."""
    try:
        c = _client()
        data = c.get_my_analytics()
    except MachinsError as e:
        _die(e.message)

    by_status = data.get("trades_by_status", {})
    click.echo("Trades by status:")
    for s, count in by_status.items():
        click.echo(f"  {s:15} {count}")

    click.echo("")
    click.echo(kv([
        ("Revenue (seller)", f"{data.get('seller_revenue', 0):.2f} CR"),
        ("Spent (buyer)", f"{data.get('buyer_spending', 0):.2f} CR"),
        ("Platform rank", f"#{data.get('platform_rank', '?')}"),
    ]))


@click.command()
def badges():
    """Show available badges and your earned ones."""
    try:
        c = _client()
        data = c.get_badges()
    except MachinsError as e:
        _die(e.message)

    earned = data.get("earned", [])
    available = data.get("available", [])

    if earned:
        click.echo("Earned badges:")
        for b in earned:
            click.echo(f"  {b.get('icon', '')} {b.get('name', '?')}: {b.get('description', '')}")

    if available:
        click.echo("\nAvailable badges:")
        for b in available:
            click.echo(f"  {b.get('icon', '')} {b.get('name', '?')}: {b.get('description', '')}")


# ── pipelines ────────────────────────────────────────────────────────

@click.group()
def pipeline():
    """Manage multi-agent pipelines."""
    pass


@pipeline.command("create")
@click.option("--name", required=True, help="Pipeline name")
@click.option("--slug", required=True, help="Unique slug")
@click.option("--description", default=None, help="Description")
@click.option("--steps", required=True, help="JSON array of steps [{listing_id, input_mapping?}]")
def pipeline_create(name: str, slug: str, description: str | None, steps: str):
    """Create a new multi-agent pipeline."""
    try:
        steps_list = json.loads(steps)
    except json.JSONDecodeError:
        _die("--steps must be valid JSON array")

    try:
        c = _client()
        p = c.create_pipeline(name, slug, steps_list, description=description)
    except MachinsError as e:
        _die(e.message)

    click.echo(f"Pipeline created: {p.get('name', name)}")
    click.echo(f"  ID:    {p.get('id', '?')}")
    click.echo(f"  Steps: {len(steps_list)}")


@pipeline.command("list")
def pipeline_list():
    """List your pipelines."""
    try:
        c = _client()
        items = c.list_pipelines()
    except MachinsError as e:
        _die(e.message)

    if not items:
        click.echo("No pipelines found.")
        return

    rows = [
        {
            "id": str(p.get("id", ""))[:8],
            "name": str(p.get("name", "?"))[:30],
            "steps": str(len(p.get("steps", []))),
            "status": p.get("status", "?"),
        }
        for p in items
    ]
    click.echo(table(rows, [
        ("id", "ID", 8),
        ("name", "Name", 30),
        ("steps", "Steps", 5),
        ("status", "Status", 8),
    ]))


@pipeline.command("execute")
@click.argument("pipeline_id")
@click.option("--input", "input_json", default=None, help="JSON input data")
def pipeline_execute(pipeline_id: str, input_json: str | None):
    """Execute a pipeline."""
    input_data = json.loads(input_json) if input_json else None
    try:
        c = _client()
        ex = c.execute_pipeline(pipeline_id, input_data)
    except MachinsError as e:
        _die(e.message)

    click.echo(f"Execution started: {ex.get('id', '?')}")
    click.echo(f"  Status: {ex.get('status', '?')}")
    click.echo(f"  Step:   {ex.get('current_step', 0) + 1}/{len(ex.get('step_results', []) or [])}")


@pipeline.command("status")
@click.argument("execution_id")
def pipeline_status(execution_id: str):
    """Check pipeline execution status."""
    try:
        c = _client()
        ex = c.get_pipeline_execution(execution_id)
    except MachinsError as e:
        _die(e.message)

    click.echo(kv([
        ("Execution", str(ex.get("id", "?"))[:8]),
        ("Status", ex.get("status", "?")),
        ("Step", f"{ex.get('current_step', 0) + 1}"),
        ("Cost", f"{ex.get('total_cost', 0)} CR"),
    ]))

    if ex.get("error"):
        click.echo(f"\n  Error: {ex['error']}")

    results = ex.get("step_results") or []
    if results:
        click.echo(f"\nStep results:")
        for i, r in enumerate(results):
            status = r.get("status", "?")
            click.echo(f"  Step {i+1}: {status}")
