package de.oaa.libredeck.web.service.tidal;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import de.oaa.libredeck.web.model.PlaylistSummary;
import de.oaa.libredeck.web.model.Track;
import de.oaa.libredeck.web.service.TrackNormalizer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
public class TidalApiClient {

    private static final String API_BASE   = "https://openapi.tidal.com/v2";
    private static final long   RATE_LIMIT = 200; // ms — Tidal is stricter

    private static final ThreadLocal<String> TOKEN_OVERRIDE      = new ThreadLocal<>();
    private static final ThreadLocal<String> COUNTRY_OVERRIDE    = new ThreadLocal<>();

    private final HttpClient   http   = HttpClient.newHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();
    private final TidalTokenStore tokenStore;

    private long lastRequest = 0;

    public void pinTokenForThread(String token, String countryCode) {
        TOKEN_OVERRIDE.set(token);
        COUNTRY_OVERRIDE.set(countryCode);
    }

    public void clearThreadToken() {
        TOKEN_OVERRIDE.remove();
        COUNTRY_OVERRIDE.remove();
    }

    public String fetchPlaylistTitle(String playlistId) throws IOException {
        String cc   = countryCode();
        JsonNode root = mapper.readTree(get(API_BASE + "/playlists/" + playlistId + "?countryCode=" + cc));
        return root.path("data").path("attributes").path("name").asText("Playlist " + playlistId);
    }

    public List<Track> fetchTracks(String playlistId) throws IOException {
        String cc      = countryCode();
        List<Track> result = new ArrayList<>();
        String url = API_BASE + "/playlists/" + playlistId + "/relationships/items"
                + "?countryCode=" + cc + "&include=items";

        while (url != null) {
            JsonNode root = mapper.readTree(get(url));
            log.debug("Tidal items page: {}", root.toString().substring(0, Math.min(500, root.toString().length())));

            // Build a lookup map from included tracks
            Map<String, JsonNode> trackMap = new HashMap<>();
            JsonNode included = root.path("included");
            if (included.isArray()) {
                for (JsonNode node : included) {
                    if ("tracks".equals(node.path("type").asText())) {
                        trackMap.put(node.path("id").asText(), node.path("attributes"));
                    }
                }
            }

            // Iterate data (item relationships)
            JsonNode data = root.path("data");
            if (data.isArray()) {
                for (JsonNode rel : data) {
                    JsonNode itemRef = rel.path("relationships").path("item").path("data");
                    if (itemRef.isMissingNode()) continue;
                    String trackId = itemRef.path("id").asText("");
                    if (trackId.isEmpty()) continue;

                    JsonNode attrs = trackMap.get(trackId);
                    if (attrs == null || attrs.isMissingNode()) continue;

                    String rawTitle  = attrs.path("title").asText("");
                    String version   = attrs.path("version").asText("");
                    if (!version.isBlank()) rawTitle = rawTitle + " (" + version + ")";

                    String rawArtist = "";
                    JsonNode artists = attrs.path("artists");
                    if (artists.isArray()) {
                        for (JsonNode a : artists) {
                            if (a.path("main").asBoolean(false) || rawArtist.isEmpty()) {
                                rawArtist = a.path("name").asText("");
                                if (a.path("main").asBoolean(false)) break;
                            }
                        }
                    }

                    String date = attrs.path("album").path("releaseDate").asText("");
                    String year = date.length() >= 4 ? date.substring(0, 4) : "";

                    String streamUrl = "https://tidal.com/browse/track/" + trackId;

                    String[] ta = TrackNormalizer.extractFeaturing(TrackNormalizer.cleanTitle(rawTitle), rawArtist);
                    result.add(new Track(trackId, ta[0], ta[1], year, streamUrl));
                }
            }

            // JSON:API cursor-based pagination via links.next
            JsonNode next = root.path("links").path("next");
            if (next.isMissingNode() || next.isNull() || next.asText("").isBlank()) {
                url = null;
            } else {
                String nextStr = next.asText();
                // next may be relative or absolute
                url = nextStr.startsWith("http") ? nextStr : API_BASE + nextStr;
            }
        }

        return result;
    }

    public List<PlaylistSummary> fetchUserPlaylists() throws IOException {
        String cc = countryCode();
        List<PlaylistSummary> result = new ArrayList<>();
        // filter[owners.id]=me → only playlists owned by the current user
        String url = API_BASE + "/playlists?filter%5Bowners.id%5D=me&countryCode=" + cc + "&include=coverArt";

        while (url != null) {
            String raw = get(url);
            log.info("Tidal playlists response: {}", raw.substring(0, Math.min(500, raw.length())));
            JsonNode root = mapper.readTree(raw);

            // Build cover art URL lookup from included images
            Map<String, String> coverUrls = new HashMap<>();
            JsonNode included = root.path("included");
            if (included.isArray()) {
                for (JsonNode node : included) {
                    if ("images".equals(node.path("type").asText())) {
                        String imgId  = node.path("id").asText();
                        String imgUrl = node.path("attributes").path("url").asText("");
                        if (!imgUrl.isEmpty()) coverUrls.put(imgId, imgUrl);
                    }
                }
            }

            JsonNode data = root.path("data");
            if (data.isArray()) {
                for (JsonNode pl : data) {
                    String id = pl.path("id").asText("");
                    if (id.isEmpty()) continue;
                    String name  = pl.path("attributes").path("name").asText("");
                    int    total = pl.path("attributes").path("numberOfTracks").asInt(0);

                    String cover = "";
                    JsonNode coverData = pl.path("relationships").path("coverArt").path("data");
                    if (coverData.isArray() && !coverData.isEmpty()) {
                        cover = coverUrls.getOrDefault(coverData.get(0).path("id").asText(""), "");
                    }

                    result.add(new PlaylistSummary(id, name, total, cover));
                }
            }

            JsonNode next = root.path("links").path("next");
            if (next.isMissingNode() || next.isNull() || next.asText("").isBlank()) {
                url = null;
            } else {
                String nextStr = next.asText();
                url = nextStr.startsWith("http") ? nextStr : API_BASE + nextStr;
            }
        }

        return result;
    }

    // ── HTTP helpers ──────────────────────────────────────────────────────────

    private synchronized String get(String url) throws IOException {
        String token = TOKEN_OVERRIDE.get();
        if (token == null) {
            if (!tokenStore.hasValidToken()) throw new TidalAuthRequiredException();
            token = tokenStore.getAccessToken();
        }
        rateLimitWait();

        HttpResponse<String> response = send(url, token);
        if (response.statusCode() == 401) {
            throw new TidalAuthRequiredException();
        }
        if (response.statusCode() != 200) {
            log.warn("Tidal API {} → {}: {}", url, response.statusCode(), response.body());
            throw new IOException("Tidal API error " + response.statusCode() + ": " + response.body());
        }
        return response.body();
    }

    private HttpResponse<String> send(String url, String token) throws IOException {
        try {
            return http.send(
                    HttpRequest.newBuilder()
                            .uri(URI.create(url))
                            .header("Authorization", "Bearer " + token)
                            .header("Accept", "application/vnd.api+json")
                            .GET().build(),
                    HttpResponse.BodyHandlers.ofString());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Request interrupted", e);
        }
    }

    private String countryCode() {
        String cc = COUNTRY_OVERRIDE.get();
        if (cc != null && !cc.isBlank()) return cc;
        cc = tokenStore.getCountryCode();
        return (cc != null && !cc.isBlank()) ? cc : "US";
    }

    private void rateLimitWait() {
        long wait = RATE_LIMIT - (System.currentTimeMillis() - lastRequest);
        if (wait > 0) {
            try { Thread.sleep(wait); } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        lastRequest = System.currentTimeMillis();
    }
}
