package de.oaa.libredeck.web.controller;

import de.oaa.libredeck.web.model.Track;
import de.oaa.libredeck.web.service.GenerationJob;
import de.oaa.libredeck.web.service.JobStore;
import de.oaa.libredeck.web.service.MusicBrainzClient;
import de.oaa.libredeck.web.service.applemusic.AppleMusicApiClient;
import de.oaa.libredeck.web.service.applemusic.AppleMusicTokenStore;
import de.oaa.libredeck.web.service.pdf.PdfCardGenerator;
import de.oaa.libredeck.web.service.pdf.QrCodeGenerator;
import de.oaa.libredeck.web.service.spotify.SpotifyApiClient;
import de.oaa.libredeck.web.service.spotify.SpotifyTokenStore;
import de.oaa.libredeck.web.service.streaming.StreamingProvider;
import de.oaa.libredeck.web.service.streaming.StreamingProviderRegistry;
import de.oaa.libredeck.web.service.tidal.TidalApiClient;
import de.oaa.libredeck.web.service.tidal.TidalTokenStore;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.Optional;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@Slf4j
@RestController
@RequestMapping("/generate")
@RequiredArgsConstructor
public class GenerateController {

    private final StreamingProviderRegistry registry;
    private final PdfCardGenerator          pdfGenerator;
    private final QrCodeGenerator           qrGenerator;
    private final JobStore                  jobStore;
    private final MusicBrainzClient         musicBrainz;
    private final SpotifyTokenStore         spotifyTokenStore;
    private final SpotifyApiClient          spotifyApiClient;
    private final TidalTokenStore           tidalTokenStore;
    private final TidalApiClient            tidalApiClient;
    private final AppleMusicTokenStore      appleMusicTokenStore;
    private final AppleMusicApiClient       appleMusicApiClient;

    /**
     * Starts an async job: fetches tracks, resolves release years via the streaming
     * provider (primary) and MusicBrainz (fallback), then enters REVIEW_READY so
     * the user can verify and adjust all years before PDF generation.
     */
    @GetMapping("/lookup")
    public Map<String, Object> lookup(@RequestParam(name = "playlistUrl") String playlistUrl) {
        // Check auth before spawning the async job
        Optional<StreamingProvider> providerOpt = registry.findForUrl(playlistUrl);
        if (providerOpt.isPresent() && "spotify".equals(providerOpt.get().getId())
                && !spotifyTokenStore.hasValidToken()) {
            String authUrl = "/spotify/auth?returnUrl=" + URLEncoder.encode(playlistUrl, StandardCharsets.UTF_8);
            return Map.of("spotifyAuthRequired", true, "authUrl", authUrl);
        }
        if (providerOpt.isPresent() && "tidal".equals(providerOpt.get().getId())
                && !tidalTokenStore.hasValidToken()) {
            String authUrl = "/tidal/auth?returnUrl=" + URLEncoder.encode(playlistUrl, StandardCharsets.UTF_8);
            return Map.of("tidalAuthRequired", true, "authUrl", authUrl);
        }
        // Library playlists require a Music User Token; catalog URLs work without auth
        if (providerOpt.isPresent() && "applemusic".equals(providerOpt.get().getId())
                && playlistUrl.contains("/library/")
                && !appleMusicTokenStore.hasToken()) {
            return Map.of("appleMusicAuthRequired", true);
        }

        GenerationJob job = jobStore.create();

        // Capture tokens on the request thread — session context won't be available on the async thread
        boolean isSpotify     = providerOpt.isPresent() && "spotify".equals(providerOpt.get().getId());
        boolean isTidal       = providerOpt.isPresent() && "tidal".equals(providerOpt.get().getId());
        boolean isAppleMusic  = providerOpt.isPresent() && "applemusic".equals(providerOpt.get().getId());
        String  pinnedToken   = isSpotify    ? spotifyTokenStore.getAccessToken()    : null;
        String  tidalToken    = isTidal      ? tidalTokenStore.getAccessToken()      : null;
        String  tidalCountry  = isTidal      ? tidalTokenStore.getCountryCode()      : null;
        String  appleMusicMut = isAppleMusic ? appleMusicTokenStore.getMusicUserToken() : null;

        CompletableFuture.runAsync(() -> {
            if (pinnedToken   != null) spotifyApiClient.pinTokenForThread(pinnedToken);
            if (tidalToken    != null) tidalApiClient.pinTokenForThread(tidalToken, tidalCountry);
            if (appleMusicMut != null) appleMusicApiClient.pinMusicUserTokenForThread(appleMusicMut);
            try {
                job.phase = GenerationJob.Phase.FETCHING;

                StreamingProvider provider = registry.findForUrl(playlistUrl)
                        .orElseThrow(() -> new IllegalArgumentException(
                                "Diese URL wird nicht unterstützt. Bitte einen Playlist-Link von Spotify, Tidal, Deezer, Apple Music oder YouTube eingeben."));

                String      playlistId = provider.extractPlaylistId(playlistUrl);
                job.playlistTitle      = provider.fetchPlaylistTitle(playlistId);
                List<Track> raw        = provider.fetchTracks(playlistId);

                if (raw.isEmpty()) throw new IllegalStateException("Die Playlist enthält keine Titel.");

                job.phase = GenerationJob.Phase.LOOKUP;
                job.total = raw.size();

                List<Track> enriched = new ArrayList<>(raw.size());
                int done = 0;
                for (Track t : raw) {
                    if (job.phase == GenerationJob.Phase.CANCELLED) return;

                    // 1. year from fetchTracks (Spotify embeds it), or provider lookup (Deezer)
                    String year = t.releaseYear().isEmpty() ? provider.fetchTrackYear(t.id()) : t.releaseYear();

                    // 2. MusicBrainz fallback
                    if (year.isEmpty()) {
                        year = musicBrainz.fetchYear(t.artist(), t.title());
                    }

                    enriched.add(new Track(t.id(), t.title(), t.artist(), year, t.streamingUrl()));
                    job.done = ++done;
                }

                job.tracks = enriched;
                job.phase  = GenerationJob.Phase.REVIEW_READY;

            } catch (Exception e) {
                log.warn("Lookup failed for job {}: {}", job.id, e.getMessage());
                job.errorMessage = e.getMessage();
                job.phase        = GenerationJob.Phase.ERROR;
            } finally {
                spotifyApiClient.clearThreadToken();
                tidalApiClient.clearThreadToken();
                appleMusicApiClient.clearThreadToken();
            }
        });

        return Map.of("jobId", job.id);
    }

    /**
     * Called after the user has reviewed all years.
     * Applies manual overrides; tracks with an empty year are excluded from the PDF.
     */
    @PostMapping("/confirm")
    public Map<String, String> confirm(@RequestBody GenerateRequest req) {
        GenerationJob job = jobStore.get(req.jobId())
                .orElseThrow(() -> new IllegalArgumentException("Job nicht gefunden: " + req.jobId()));

        if (job.phase != GenerationJob.Phase.REVIEW_READY) {
            throw new IllegalStateException("Job ist nicht im Review-Status.");
        }

        Map<String, String> yearOverrides   = req.years()   != null ? req.years()   : Map.of();
        Map<String, String> artistOverrides = req.artists() != null ? req.artists() : Map.of();
        Map<String, String> titleOverrides  = req.titles()  != null ? req.titles()  : Map.of();

        List<Track> confirmed = new ArrayList<>();
        for (Track t : job.tracks) {
            String year   = yearOverrides.getOrDefault(t.id(), t.releaseYear()).trim();
            if (!year.isEmpty()) {
                String artist = artistOverrides.getOrDefault(t.id(), t.artist()).trim();
                String title  = titleOverrides.getOrDefault(t.id(), t.title()).trim();
                confirmed.add(new Track(t.id(), title, artist, year, t.streamingUrl()));
            }
        }

        if (confirmed.isEmpty()) {
            job.errorMessage = "Keine Karten zum Erstellen – alle Titel ohne Jahr wurden ausgeschlossen.";
            job.phase        = GenerationJob.Phase.ERROR;
            return Map.of("jobId", job.id);
        }

        CompletableFuture.runAsync(() -> {
            try {
                buildPdf(job, confirmed);
            } catch (Exception e) {
                log.warn("PDF generation failed for job {}: {}", job.id, e.getMessage());
                job.errorMessage = e.getMessage();
                job.phase        = GenerationJob.Phase.ERROR;
            }
        });

        return Map.of("jobId", job.id);
    }

    /** Returns the current status of a job for polling. */
    @GetMapping("/status")
    public Map<String, Object> status(@RequestParam(name = "jobId") String jobId) {
        GenerationJob job = jobStore.get(jobId)
                .orElseThrow(() -> new IllegalArgumentException("Job nicht gefunden: " + jobId));

        Map<String, Object> resp = new HashMap<>();
        resp.put("phase", job.phase.name());
        resp.put("done",  job.done);
        resp.put("total", job.total);
        resp.put("ready", job.phase == GenerationJob.Phase.DONE);
        resp.put("error", job.errorMessage != null ? job.errorMessage : "");

        if (job.phase == GenerationJob.Phase.REVIEW_READY && job.tracks != null) {
            List<Map<String, String>> tracks = job.tracks.stream()
                    .map(t -> Map.of(
                            "id",          t.id(),
                            "artist",      t.artist(),
                            "title",       t.title(),
                            "year",        t.releaseYear(),
                            "streamingUrl", t.streamingUrl()))
                    .toList();
            resp.put("tracks", tracks);
        }

        return resp;
    }

    /** Generates a QR code PNG for the given URL (used by the play dialog). */
    @GetMapping("/qr")
    public void qr(@RequestParam("url") String url, HttpServletResponse response) throws IOException {
        BufferedImage img = qrGenerator.generate(url, 200);
        response.setContentType("image/png");
        response.setHeader("Cache-Control", "public, max-age=86400");
        ImageIO.write(img, "png", response.getOutputStream());
    }

    /** Looks up a release year for a single track via MusicBrainz (used by the per-row "Prüfen" button). */
    @GetMapping("/check-year")
    public Map<String, String> checkYear(@RequestParam(name = "artist") String artist,
                                         @RequestParam(name = "title")  String title) {
        return Map.of("year", musicBrainz.fetchYear(artist, title));
    }

    /** Cancels a running job. Safe to call at any phase. */
    @PostMapping("/cancel")
    public Map<String, String> cancel(@RequestParam(name = "jobId") String jobId) {
        jobStore.get(jobId).ifPresent(job -> {
            job.phase = GenerationJob.Phase.CANCELLED;
            jobStore.remove(jobId);
        });
        return Map.of("jobId", jobId);
    }

    /** Streams the finished PDF and removes the job from the store. */
    @GetMapping("/download")
    public void download(@RequestParam(name = "jobId") String jobId,
                         HttpServletResponse response) throws IOException {
        GenerationJob job = jobStore.get(jobId)
                .orElseThrow(() -> new IllegalArgumentException("Job nicht gefunden oder bereits abgelaufen."));

        if (job.phase != GenerationJob.Phase.DONE) {
            response.sendError(HttpServletResponse.SC_CONFLICT, "Job ist noch nicht fertig.");
            return;
        }

        byte[] pdf = job.pdf;
        jobStore.remove(jobId);

        response.setContentType(MediaType.APPLICATION_PDF_VALUE);
        response.setHeader("Content-Disposition", "attachment; filename=\"" + job.filename + "\"");
        response.setContentLength(pdf.length);
        response.getOutputStream().write(pdf);
    }

    private void buildPdf(GenerationJob job, List<Track> tracks) throws IOException {
        job.phase = GenerationJob.Phase.GENERATING;
        job.done  = 0;
        job.total = tracks.size();

        byte[] pdf = pdfGenerator.generate(job.playlistTitle, tracks, n -> job.done = n);

        job.filename = job.playlistTitle.replaceAll("[^a-zA-Z0-9_\\-]", "_").toLowerCase() + ".pdf";
        job.pdf      = pdf;
        job.phase    = GenerationJob.Phase.DONE;
    }
}
