local utils = require("mp.utils")
local options = require("mp.options")
local msg = require("mp.msg")

local o = {
	youtube_csv = "~/nixos-config/dotfiles/mpv/youtube.csv",
	twitch_csv = "~/nixos-config/dotfiles/mpv/twitch.csv",
	key = "alt+h",
	-- nombre de vidéos à charger
	per_channel = 3,
	max_videos = 150,
	channel_videos = 400,
	-- live check twitch (ne fonctionne pas pour le moment)
	twitch_live_check = true,
}
options.read_options(o, "feeds")

local cache = { youtube = nil, twitch = nil, channels = {} }

local function expand(path)
	return mp.command_native({ "expand-path", path })
end

local function unescape(s)
	if not s then
		return ""
	end
	s = s:gsub("&lt;", "<"):gsub("&gt;", ">"):gsub("&quot;", '"')
	s = s:gsub("&#39;", "'"):gsub("&apos;", "'"):gsub("&amp;", "&")
	return s
end

local function trim(s)
	return (s or ""):gsub("^%s+", ""):gsub("%s+$", "")
end

local function format_duration(sec)
	if type(sec) ~= "number" or sec <= 0 then
		return ""
	end
	sec = math.floor(sec)
	local h = math.floor(sec / 3600)
	local m = math.floor((sec % 3600) / 60)
	local s = sec % 60
	if h > 0 then
		return string.format("%d:%02d:%02d", h, m, s)
	end
	return string.format("%d:%02d", m, s)
end

local function open_menu(m)
	mp.commandv("script-message-to", "uosc", "open-menu", utils.format_json(m))
end

local function update_menu(m)
	mp.commandv("script-message-to", "uosc", "update-menu", utils.format_json(m))
end

local function loading_menu(type_, title)
	open_menu({
		type = type_,
		title = title,
		items = { { title = "Chargement…", icon = "spinner", selectable = false, keep_open = true } },
	})
end

local function read_lines(path)
	local lines = {}
	local full = expand(path)
	local f = io.open(full, "r")
	if not f then
		msg.warn("CSV illisible : " .. tostring(full))
		return lines
	end
	for line in f:lines() do
		lines[#lines + 1] = (line:gsub("\r$", ""))
	end
	f:close()
	return lines
end

--------------------------------------------------- Lecture

mp.register_script_message("feeds-play", function(url)
	mp.commandv("loadfile", url, "replace")
end)

mp.register_script_message("feeds-twitch", function(login)
	mp.osd_message("Ouverture de " .. login .. " via streamlink…", 3)
	mp.command_native_async({
		name = "subprocess",
		args = { "streamlink", "--player=mpv", "--twitch-low-latency", "twitch.tv/" .. login, "best" },
		detach = true,
		-- sans ceci, mpv tue le processus dès qu'aucune vidéo n'est en lecture
		playback_only = false,
	}, function() end)
end)

--------------------------------------------------- CSV YouTube

local function read_youtube_channels()
	local channels = {}
	for _, line in ipairs(read_lines(o.youtube_csv)) do
		local id = line:match("^([%w_-]+)")
		if id and id:match("^UC") then -- écarte l'en-tête du CSV
			local title = line:match("^[^,]*,[^,]*,(.*)$") or ""
			title = trim(title)
			local inner = title:match('^"(.*)"$')
			if inner then
				title = inner:gsub('""', '"')
			end
			if title == "" then
				title = id
			end
			channels[#channels + 1] = { id = id, title = title }
		end
	end
	table.sort(channels, function(a, b)
		return a.title:lower() < b.title:lower()
	end)
	return channels
end

--------------------------------------------------- Abonnements (RSS)

local function youtube_menu_data(videos)
	local items = {}
	for _, v in ipairs(videos) do
		items[#items + 1] = {
			title = v.author .. " — " .. v.title,
			hint = v.published:sub(1, 10),
			value = { "script-message", "feeds-play", v.url },
		}
	end
	if #items == 0 then
		items = {
			{
				title = "Aucune vidéo récupérée — voir les logs (--msg-level=feeds=info)",
				selectable = false,
			},
		}
	end
	return { type = "yt_feed", title = "Abonnements YouTube", items = items }
end

local function parse_feeds(blob)
	local videos, seen_count = {}, {}
	for entry in blob:gmatch("<entry>(.-)</entry>") do
		local id = entry:match("<yt:videoId>(.-)</yt:videoId>")
		local title = entry:match("<title>(.-)</title>")
		local author = entry:match("<author>.-<name>(.-)</name>")
		local published = entry:match("<published>(.-)</published>")
		if id and title and published then
			videos[#videos + 1] = {
				url = "https://www.youtube.com/watch?v=" .. id,
				title = unescape(title),
				author = unescape(author or "?"),
				published = published,
			}
		end
	end

	-- les dates ISO 8601 se trient correctement comme de simples chaînes
	table.sort(videos, function(a, b)
		return a.published > b.published
	end)

	local kept = {}
	for _, v in ipairs(videos) do
		local n = (seen_count[v.author] or 0)
		if n < o.per_channel then
			seen_count[v.author] = n + 1
			kept[#kept + 1] = v
			if #kept >= o.max_videos then
				break
			end
		end
	end
	return kept
end

local function youtube_menu(force)
	if cache.youtube and not force then
		open_menu(youtube_menu_data(cache.youtube))
		return
	end

	local channels = read_youtube_channels()
	local args = { "curl", "-s", "--parallel", "--parallel-max", "16", "-m", "25" }
	for _, c in ipairs(channels) do
		args[#args + 1] = "https://www.youtube.com/feeds/videos.xml?channel_id=" .. c.id
	end

	if #channels == 0 then
		open_menu({
			type = "yt_feed",
			title = "Abonnements YouTube",
			items = { { title = "Aucune chaîne lue dans " .. o.youtube_csv, selectable = false } },
		})
		return
	end

	msg.info(#channels .. " chaînes à interroger")
	loading_menu("yt_feed", "Abonnements YouTube")

	mp.command_native_async({
		name = "subprocess",
		args = args,
		capture_stdout = true,
		playback_only = false,
	}, function(_, res)
		local videos = {}
		if res and res.stdout and #res.stdout > 0 then
			videos = parse_feeds(res.stdout)
		end
		msg.info(
			string.format(
				"curl status=%s, %d octets, %d vidéos",
				tostring(res and res.status),
				res and res.stdout and #res.stdout or 0,
				#videos
			)
		)
		cache.youtube = videos
		update_menu(youtube_menu_data(videos))
	end)
end

--------------------------------------------------- Parcourir une chaîne

local function channel_videos_menu_data(name, videos)
	local items = {}
	for _, v in ipairs(videos) do
		items[#items + 1] = {
			title = v.title,
			hint = v.duration,
			value = { "script-message", "feeds-play", v.url },
		}
	end
	if #items == 0 then
		items = { { title = "Rien récupéré — voir les logs (--msg-level=feeds=info)", selectable = false } }
	end
	return { type = "yt_channel", title = name, items = items }
end

local function channel_videos_menu(id, name)
	if cache.channels[id] then
		open_menu(channel_videos_menu_data(name, cache.channels[id]))
		return
	end

	loading_menu("yt_channel", name)

	mp.command_native_async({
		name = "subprocess",
		args = {
			"yt-dlp",
			"--flat-playlist",
			"--dump-single-json",
			"--no-warnings",
			"--playlist-items",
			"1:" .. tostring(o.channel_videos),
			"https://www.youtube.com/channel/" .. id .. "/videos",
		},
		capture_stdout = true,
		playback_only = false,
	}, function(_, res)
		local videos = {}
		if res and res.stdout and #res.stdout > 0 then
			local ok, data = pcall(utils.parse_json, res.stdout)
			if ok and data and data.entries then
				for _, e in ipairs(data.entries) do
					if e.id then
						videos[#videos + 1] = {
							title = e.title or e.id,
							duration = format_duration(e.duration),
							url = "https://www.youtube.com/watch?v=" .. e.id,
						}
					end
				end
			end
		end
		msg.info(
			string.format(
				"yt-dlp status=%s, %d octets, %d vidéos (%s)",
				tostring(res and res.status),
				res and res.stdout and #res.stdout or 0,
				#videos,
				name
			)
		)
		cache.channels[id] = videos
		update_menu(channel_videos_menu_data(name, videos))
	end)
end

mp.register_script_message("feeds-channel", function(id, name)
	channel_videos_menu(id, name)
end)

local function channels_menu()
	local channels = read_youtube_channels()
	local items = {}
	for _, c in ipairs(channels) do
		items[#items + 1] = {
			title = c.title,
			value = { "script-message", "feeds-channel", c.id, c.title },
		}
	end
	if #items == 0 then
		items = { { title = "Aucune chaîne lue dans " .. o.youtube_csv, selectable = false } }
	end
	-- tape simplement quelques lettres pour filtrer la liste
	open_menu({ type = "yt_channels", title = "Parcourir une chaîne", items = items })
end

--------------------------------------------------- Twitch

local function twitch_menu_data(channels)
	local items = {}
	for _, c in ipairs(channels) do
		local mark = ""
		if c.live == true then
			mark = "🔴 "
		elseif c.live == false then
			mark = "· "
		end
		items[#items + 1] = {
			title = mark .. (c.label ~= "" and c.label or c.login),
			hint = c.live == true and "en direct" or nil,
			value = { "script-message", "feeds-twitch", c.login },
		}
	end
	if #items == 0 then
		items = { { title = "Aucune chaîne lue dans " .. o.twitch_csv, selectable = false } }
	end
	return { type = "twitch_feed", title = "Twitch", items = items }
end

local function twitch_menu(force)
	if cache.twitch and not force then
		open_menu(twitch_menu_data(cache.twitch))
		return
	end

	local channels = {}
	for _, line in ipairs(read_lines(o.twitch_csv)) do
		local login = line:match("^([%w_][%w_]*)")
		if login and login:lower() ~= "login" then -- ignore l'en-tête éventuel
			local label = trim(line:match("^[^,]*,(.*)$") or "")
			channels[#channels + 1] = { login = login, label = label, live = nil }
		end
	end

	if #channels == 0 or not o.twitch_live_check then
		cache.twitch = channels
		open_menu(twitch_menu_data(channels))
		return
	end

	loading_menu("twitch_feed", "Twitch")

	local logins = {}
	for _, c in ipairs(channels) do
		logins[#logins + 1] = c.login
	end

	local script = [[
for c in ]] .. table.concat(logins, " ") .. [[; do
  ( if curl -sL -m 8 "https://www.twitch.tv/$c" | grep -q '"isLiveBroadcast":true'; then
      echo "$c live"
    else
      echo "$c off"
    fi ) &
done
wait
]]

	mp.command_native_async({
		name = "subprocess",
		args = { "sh", "-c", script },
		capture_stdout = true,
		playback_only = false,
	}, function(_, res)
		local nb_live = 0
		if res and res.stdout and #res.stdout > 0 then
			local status = {}
			for login, state in res.stdout:gmatch("(%S+)%s+(%S+)") do
				status[login] = (state == "live")
			end
			for _, c in ipairs(channels) do
				if status[c.login] ~= nil then
					c.live = status[c.login]
					if c.live then
						nb_live = nb_live + 1
					end
				end
			end
			table.sort(channels, function(a, b)
				if a.live ~= b.live then
					return a.live == true
				end
				return a.login < b.login
			end)
		end
		msg.info(
			string.format(
				"twitch status=%s, %d chaînes, %d en direct",
				tostring(res and res.status),
				#channels,
				nb_live
			)
		)
		cache.twitch = channels
		update_menu(twitch_menu_data(channels))
	end)
end

--------------------------------------------------- Menu racine

mp.register_script_message("feeds-open", function(which, force)
	local refresh = (force == "refresh")
	if which == "youtube" then
		youtube_menu(refresh)
	elseif which == "channels" then
		channels_menu()
	elseif which == "twitch" then
		twitch_menu(refresh)
	end
end)

mp.add_key_binding(o.key, "feeds-menu", function()
	open_menu({
		type = "feeds",
		title = "Chaînes",
		items = {
			{
				title = "Abonnements YouTube",
				icon = "subscriptions",
				value = { "script-message", "feeds-open", "youtube" },
			},
			{
				title = "Parcourir une chaîne",
				icon = "video_library",
				value = { "script-message", "feeds-open", "channels" },
			},
			{
				title = "Twitch",
				icon = "live_tv",
				value = { "script-message", "feeds-open", "twitch" },
			},
			{
				title = "Rafraîchir YouTube",
				icon = "refresh",
				muted = true,
				value = { "script-message", "feeds-open", "youtube", "refresh" },
			},
			{
				title = "Rafraîchir Twitch",
				icon = "refresh",
				muted = true,
				value = { "script-message", "feeds-open", "twitch", "refresh" },
			},
		},
	})
end)
