package mcpserver

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/gitcoder89431/tuitube/internal/agentlog"
	"github.com/gitcoder89431/tuitube/internal/db"
	"github.com/gitcoder89431/tuitube/internal/player"
	tubesync "github.com/gitcoder89431/tuitube/internal/sync"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

type Server struct {
	database *db.DB
	player   *player.Player
}

func New(database *db.DB, p *player.Player) *Server {
	return &Server{database: database, player: p}
}

const onboarding = `# tuitube — Agent Onboarding

tuitube is a terminal music player for curated YouTube channels. It streams audio via mpv, manages a local SQLite library of 8000+ tracks, and exposes this MCP server so agents can control playback, curate playlists, and manage the library.

## What you can do

- Search and play tracks: search_tracks → play_track
- Create and populate playlists: create_playlist → add_to_playlist
- Add YouTube channels: add_station(sync_now: true) → done
- Control playback: stop_playback, toggle_favorite

## Tool usage notes

- Always call search_tracks first to get track IDs before playing or favoriting
- add_to_playlist accepts an array of track_ids — batch in one call
- search_tracks with empty query returns recent tracks
- Station IDs come from list_stations
- add_station with sync_now: true pulls tracks immediately and returns inserted count
- sync_station omitting station_id syncs all stations

## Data model

- Tracks belong to stations (YouTube channels)
- Playlists contain tracks via track IDs
- Favorites playlist is always id=1
- play_track streams via mpv in the background — no TUI required

## Checking state

- tuitube status --json → {"playing":bool,"paused":bool,"youtube_id":"...","title":"...","artist":"...","time_pos":0.0,"duration":0.0}
- tuitube doctor → exits 0 if healthy, 1 with report if mpv/yt-dlp/DB missing`

func (s *Server) Serve() error {
	srv := server.NewMCPServer(
		"tuitube",
		"1.0.0",
		server.WithToolCapabilities(false),
		server.WithResourceCapabilities(false, false),
	)

	srv.AddResource(
		mcp.NewResource("tuitube://onboarding", "Agent Onboarding",
			mcp.WithResourceDescription("Usage guide and data model for AI agents"),
			mcp.WithMIMEType("text/markdown"),
		),
		func(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
			return []mcp.ResourceContents{
				mcp.TextResourceContents{URI: "tuitube://onboarding", MIMEType: "text/markdown", Text: onboarding},
			}, nil
		},
	)

	srv.AddTool(mcp.NewTool("search_tracks",
		mcp.WithDescription("Search the music library. Returns tracks matching query. Leave query empty to list recent tracks."),
		mcp.WithString("query", mcp.Description("Search query (artist, title, or both)")),
		mcp.WithBoolean("favorites_only", mcp.Description("Only return favorited tracks")),
		mcp.WithNumber("limit", mcp.Description("Max results (default 30)")),
		mcp.WithString("station_id", mcp.Description("Filter to a single station (from list_stations)")),
	), mcp.NewTypedToolHandler(s.searchTracks))

	srv.AddTool(mcp.NewTool("list_stations",
		mcp.WithDescription("List all synced YouTube channel stations"),
	), mcp.NewTypedToolHandler(s.listStations))

	srv.AddTool(mcp.NewTool("sync_station",
		mcp.WithDescription("Pull new tracks from YouTube for one or all stations via yt-dlp"),
		mcp.WithString("station_id", mcp.Description("Station ID to sync (omit to sync all)")),
	), mcp.NewTypedToolHandler(s.syncStation))

	srv.AddTool(mcp.NewTool("add_station",
		mcp.WithDescription("Add a new YouTube channel station and optionally sync it immediately"),
		mcp.WithString("url", mcp.Required(), mcp.Description("YouTube channel URL (any format: @handle, /channel/UC..., /c/, /user/)")),
		mcp.WithString("name", mcp.Required(), mcp.Description("Display name for this station")),
		mcp.WithBoolean("sync_now", mcp.Description("Immediately sync all tracks after adding (default false)")),
	), mcp.NewTypedToolHandler(s.addStation))

	srv.AddTool(mcp.NewTool("play_track",
		mcp.WithDescription("Stream a track via mpv (no video, background). Stops any currently playing track first."),
		mcp.WithString("youtube_id", mcp.Required(), mcp.Description("YouTube video ID")),
		mcp.WithString("title", mcp.Description("Track title (for display only)")),
	), mcp.NewTypedToolHandler(s.playTrack))

	srv.AddTool(mcp.NewTool("stop_playback",
		mcp.WithDescription("Stop the currently playing track"),
	), mcp.NewTypedToolHandler(s.stopPlayback))

	srv.AddTool(mcp.NewTool("toggle_favorite",
		mcp.WithDescription("Toggle the favorite status of a track"),
		mcp.WithString("track_id", mcp.Required(), mcp.Description("Track ID (from search_tracks result)")),
	), mcp.NewTypedToolHandler(s.toggleFavorite))

	srv.AddTool(mcp.NewTool("list_playlists",
		mcp.WithDescription("List all playlists (Favorites is always id=1)"),
	), mcp.NewTypedToolHandler(s.listPlaylists))

	srv.AddTool(mcp.NewTool("create_playlist",
		mcp.WithDescription("Create a new playlist"),
		mcp.WithString("name", mcp.Required(), mcp.Description("Playlist name")),
	), mcp.NewTypedToolHandler(s.createPlaylist))

	srv.AddTool(mcp.NewTool("add_to_playlist",
		mcp.WithDescription("Add one or more tracks to a playlist in a single call"),
		mcp.WithNumber("playlist_id", mcp.Required(), mcp.Description("Playlist ID (from list_playlists)")),
		mcp.WithArray("track_ids", mcp.Required(), mcp.Description("Array of track IDs to add (from search_tracks)"), mcp.Items(map[string]any{"type": "string"})),
	), mcp.NewTypedToolHandler(s.addToPlaylist))

	srv.AddTool(mcp.NewTool("list_playlist_tracks",
		mcp.WithDescription("List tracks in a playlist"),
		mcp.WithNumber("playlist_id", mcp.Required(), mcp.Description("Playlist ID")),
	), mcp.NewTypedToolHandler(s.listPlaylistTracks))

	srv.AddTool(mcp.NewTool("remove_from_playlist",
		mcp.WithDescription("Remove one or more tracks from a playlist"),
		mcp.WithNumber("playlist_id", mcp.Required(), mcp.Description("Playlist ID")),
		mcp.WithArray("track_ids", mcp.Required(), mcp.Description("Array of track IDs to remove"), mcp.Items(map[string]any{"type": "string"})),
	), mcp.NewTypedToolHandler(s.removeFromPlaylist))

	srv.AddTool(mcp.NewTool("get_now_playing",
		mcp.WithDescription("Get current playback state: track info, position, duration, and paused state"),
	), mcp.NewTypedToolHandler(s.getNowPlaying))

	return server.ServeStdio(srv)
}

// --- tool args structs ---

type searchArgs struct {
	Query         string  `json:"query"`
	FavoritesOnly bool    `json:"favorites_only"`
	Limit         float64 `json:"limit"`
	StationID     string  `json:"station_id"`
}

type syncArgs struct {
	StationID string `json:"station_id"`
}

type addStationArgs struct {
	URL     string `json:"url"`
	Name    string `json:"name"`
	SyncNow bool   `json:"sync_now"`
}

type playArgs struct {
	YoutubeID string `json:"youtube_id"`
	Title     string `json:"title"`
}

type toggleFavArgs struct {
	TrackID string `json:"track_id"`
}

type createPlaylistArgs struct {
	Name string `json:"name"`
}

type playlistTrackArgs struct {
	PlaylistID float64  `json:"playlist_id"`
	TrackIDs   []string `json:"track_ids"`
}

type listPlaylistArgs struct {
	PlaylistID float64 `json:"playlist_id"`
}

// --- handlers ---

func (s *Server) searchTracks(_ context.Context, _ mcp.CallToolRequest, args searchArgs) (*mcp.CallToolResult, error) {
	limit := int(args.Limit)
	if limit <= 0 {
		limit = 30
	}
	tracks, err := s.database.ListTracks(args.Query, args.FavoritesOnly, limit, args.StationID)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	return jsonResult(tracks)
}

func (s *Server) listStations(_ context.Context, _ mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, error) {
	stations, err := s.database.ListStations()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	return jsonResult(stations)
}

func (s *Server) syncStation(_ context.Context, _ mcp.CallToolRequest, args syncArgs) (*mcp.CallToolResult, error) {
	stations, err := s.database.ListStations()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	var results []map[string]any
	total := 0
	for _, st := range stations {
		if args.StationID != "" && st.ID != args.StationID {
			continue
		}
		var buf syncWriter
		n, err := tubesync.Station(s.database, st, &buf)
		result := map[string]any{"station": st.Name, "inserted": n, "log": buf.String()}
		if err != nil {
			result["error"] = err.Error()
		}
		results = append(results, result)
		total += n
	}

	if total > 0 {
		if err := s.database.RebuildFTS(); err != nil {
			return mcp.NewToolResultError("fts rebuild: " + err.Error()), nil
		}
	}
	if total > 0 {
		agentlog.Write(fmt.Sprintf("↻ synced %d new tracks", total))
	}
	return jsonResult(map[string]any{"total_inserted": total, "stations": results})
}

func (s *Server) addStation(_ context.Context, _ mcp.CallToolRequest, args addStationArgs) (*mcp.CallToolResult, error) {
	var buf syncWriter
	station, err := tubesync.DiscoverStation(args.URL, &buf)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	station.Name = args.Name
	if err := s.database.UpsertStation(station); err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	result := map[string]any{
		"station":  station,
		"log":      buf.String(),
		"inserted": 0,
	}

	if args.SyncNow {
		var buf2 syncWriter
		n, err := tubesync.Station(s.database, station, &buf2)
		result["inserted"] = n
		result["sync_log"] = buf2.String()
		if err != nil {
			result["sync_error"] = err.Error()
		}
		if n > 0 {
			_ = s.database.RebuildFTS()
			agentlog.Write(fmt.Sprintf("↻ synced %s: %d new tracks", args.Name, n))
		}
	}

	return jsonResult(result)
}

func (s *Server) playTrack(ctx context.Context, _ mcp.CallToolRequest, args playArgs) (*mcp.CallToolResult, error) {
	if err := s.player.Play(ctx, args.YoutubeID, args.Title, "", "", 0); err != nil {
		return mcp.NewToolResultError("mpv: " + err.Error()), nil
	}
	label := args.Title
	if label == "" {
		label = args.YoutubeID
	}
	agentlog.Write("▶ playing: " + label)
	return mcp.NewToolResultText(fmt.Sprintf("now playing: %s", label)), nil
}

func (s *Server) stopPlayback(ctx context.Context, _ mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, error) {
	np := s.player.NowPlaying()
	s.player.Stop(ctx)
	if np == nil {
		return mcp.NewToolResultText("nothing was playing"), nil
	}
	agentlog.Write("⏹ stopped: " + np.Title)
	return mcp.NewToolResultText("stopped: " + np.Title), nil
}

func (s *Server) toggleFavorite(_ context.Context, _ mcp.CallToolRequest, args toggleFavArgs) (*mcp.CallToolResult, error) {
	isFav, err := s.database.ToggleFavorite(args.TrackID)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	state := "removed from favorites"
	if isFav {
		state = "added to favorites"
	}
	agentlog.Write("♥ " + state + ": " + args.TrackID)
	return mcp.NewToolResultText(state), nil
}

func (s *Server) listPlaylists(_ context.Context, _ mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, error) {
	playlists, err := s.database.ListPlaylists()
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	return jsonResult(playlists)
}

func (s *Server) createPlaylist(_ context.Context, _ mcp.CallToolRequest, args createPlaylistArgs) (*mcp.CallToolResult, error) {
	id, err := s.database.CreatePlaylist(args.Name)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	agentlog.Write(fmt.Sprintf("♪ created playlist: %s (id=%d)", args.Name, id))
	return jsonResult(map[string]any{"id": id, "name": args.Name})
}

func (s *Server) addToPlaylist(_ context.Context, _ mcp.CallToolRequest, args playlistTrackArgs) (*mcp.CallToolResult, error) {
	added, err := s.database.AddToPlaylistBatch(int64(args.PlaylistID), args.TrackIDs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("batch insert failed (rolled back): %v", err)), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("added %d tracks", added)), nil
}

func (s *Server) listPlaylistTracks(_ context.Context, _ mcp.CallToolRequest, args listPlaylistArgs) (*mcp.CallToolResult, error) {
	tracks, err := s.database.ListPlaylistTracks(int64(args.PlaylistID))
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	return jsonResult(tracks)
}

func (s *Server) removeFromPlaylist(_ context.Context, _ mcp.CallToolRequest, args playlistTrackArgs) (*mcp.CallToolResult, error) {
	pid := int64(args.PlaylistID)
	removed := 0
	for _, id := range args.TrackIDs {
		if err := s.database.RemoveFromPlaylist(pid, id); err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("failed on %s: %v", id, err)), nil
		}
		removed++
	}
	return mcp.NewToolResultText(fmt.Sprintf("removed %d tracks", removed)), nil
}

func (s *Server) getNowPlaying(ctx context.Context, _ mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, error) {
	state := s.player.NowPlaying()
	if state == nil {
		return jsonResult(map[string]any{"playing": false})
	}
	tp, dur := s.player.Progress(ctx)
	return jsonResult(map[string]any{
		"playing":    state.Playing,
		"paused":     state.Paused,
		"youtube_id": state.YoutubeID,
		"title":      state.Title,
		"artist":     state.Artist,
		"time_pos":   tp,
		"duration":   dur,
	})
}

// --- helpers ---

func jsonResult(v any) (*mcp.CallToolResult, error) {
	b, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	return mcp.NewToolResultText(string(b)), nil
}

// syncWriter captures sync log output efficiently.
type syncWriter struct{ b strings.Builder }

func (w *syncWriter) Write(p []byte) (int, error) { return w.b.Write(p) }
func (w *syncWriter) String() string              { return w.b.String() }
