package com.nadi.player.nativeplayer;

import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;

import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.session.MediaSession;
import androidx.media3.session.MediaSessionService;

import com.nadi.player.MainActivity;

import java.util.HashMap;
import java.util.Map;

/**
 * Foreground media service that owns the ExoPlayer and a MediaSession.
 *
 * Why a service?
 *   • Audio keeps playing when the screen is locked or the user switches
 *     to WhatsApp / any other app, because Android keeps the foreground
 *     service alive (with its visible notification).
 *   • Picture-in-Picture works "for free" because the PlayerActivity
 *     just attaches to the running session via MediaController. When the
 *     user closes the PiP window we stop the service, which releases the
 *     player.
 *   • The MediaSession automatically surfaces a "Now Playing" notification
 *     with play / pause / stop buttons, plus integrates with lock screen,
 *     media buttons (Bluetooth headset, car), Google Assistant.
 *
 * The activity passes the URL + user-agent + headers as Intent extras
 * when it calls startForegroundService(). On API 33+ this also asks for
 * POST_NOTIFICATIONS via a runtime permission flow handled by the
 * Activity (the service can still post — Android shows the notification
 * silently if permission is denied).
 */
@OptIn(markerClass = UnstableApi.class)
public class PlaybackService extends MediaSessionService {

    public static final String EXTRA_URL = "url";
    public static final String EXTRA_USER_AGENT = "user_agent";
    public static final String EXTRA_TITLE = "title";
    public static final String EXTRA_IS_LIVE = "is_live";
    public static final String EXTRA_HEADER_KEYS = "header_keys";
    public static final String EXTRA_HEADER_VALS = "header_vals";
    /** When true, swap the MediaItem on the existing ExoPlayer instead of
     *  rebuilding it. Used by fast channel zapping — UA and headers stay
     *  the same as the original launch (same portal, same auth). */
    public static final String EXTRA_ZAP_ONLY = "zap_only";

    /** Explicit "next channel" intent — used by the home-screen widget
     *  and any other external trigger. Routes through the same zap path
     *  as the lock-screen ⏭ button (i.e. {@link ChannelAwarePlayer}). */
    public static final String ACTION_ZAP_NEXT = "com.nadi.player.action.ZAP_NEXT";
    /** Explicit "previous channel" intent — see {@link #ACTION_ZAP_NEXT}. */
    public static final String ACTION_ZAP_PREV = "com.nadi.player.action.ZAP_PREV";
    /** BUG FIX (2026-01): explicit "stop everything" intent. Sent by
     *  {@link PlayerActivity#teardownPlayback()} when the PiP window is
     *  dismissed via [X]. The activity's stopService() alone is not
     *  reliable for a foreground MediaSessionService — Android may
     *  defer destruction → audio keeps playing in the background.
     *  Handling this action explicitly releases the player and calls
     *  stopSelf() so the foreground notification + ExoPlayer go away
     *  instantly. */
    public static final String ACTION_STOP_PLAYBACK =
            "com.nadi.player.action.STOP_PLAYBACK";

    @Nullable private MediaSession session;

    @Override
    public void onCreate() {
        super.onCreate();
        ExoPlayer player = buildPlayer(
                "IPTVSmartersPlayer/3.1.6 (Linux; Android) ExoPlayerLib/2.19.1",
                new HashMap<>()
        );
        // Keep playing while the user is in another app (audio-only).
        player.setHandleAudioBecomingNoisy(true);
        // Tell ExoPlayer to manage audio focus for us — pauses on a
        // phone call, ducks for nav prompts, etc.
        player.setAudioAttributes(
                new androidx.media3.common.AudioAttributes.Builder()
                        .setContentType(androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE)
                        .setUsage(androidx.media3.common.C.USAGE_MEDIA)
                        .build(),
                /* handleAudioFocus = */ true
        );

        // Tapping the notification opens MainActivity (Capacitor host).
        Intent launch = new Intent(this, MainActivity.class);
        launch.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        int piFlags = PendingIntent.FLAG_UPDATE_CURRENT;
        if (Build.VERSION.SDK_INT >= 23) {
            piFlags |= PendingIntent.FLAG_IMMUTABLE;
        }
        PendingIntent sessionActivity =
                PendingIntent.getActivity(this, 0, launch, piFlags);

        session = new MediaSession.Builder(this, new ChannelAwarePlayer(player))
                .setSessionActivity(sessionActivity)
                .build();
    }

    @Nullable
    @Override
    public MediaSession onGetSession(@androidx.annotation.NonNull MediaSession.ControllerInfo info) {
        return session;
    }

    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        if (intent != null) {
            String action = intent.getAction();
            if (ACTION_ZAP_NEXT.equals(action)) {
                handleZap(+1);
            } else if (ACTION_ZAP_PREV.equals(action)) {
                handleZap(-1);
            } else if (ACTION_STOP_PLAYBACK.equals(action)) {
                // BUG FIX (2026-01) — PiP [X] audio leak.
                //
                // Triggered from PlayerActivity.teardownPlayback() when
                // the user dismisses the PiP window via [X]. We must
                // synchronously release ExoPlayer + drop the foreground
                // notification + tear down the MediaSession, because
                // Android may not call our onDestroy() for several
                // seconds (or at all) when the activity is being
                // stopped from PiP state — and during that gap, the
                // ExoPlayer instance keeps decoding audio in the
                // background.
                //
                // Synchronous teardown sequence (Media3 docs):
                //   1. Stop the player (idles MediaSession)
                //   2. Release the player (frees decoders + audio focus)
                //   3. Release the MediaSession (drops controller bindings)
                //   4. stopForeground(STOP_FOREGROUND_REMOVE) (kills notification)
                //   5. stopSelf() (queues service onDestroy)
                if (session != null) {
                    try {
                        Player p = session.getPlayer();
                        p.setPlayWhenReady(false);
                        p.stop();
                        p.clearMediaItems();
                        // CRITICAL: explicit release() drops the audio
                        // focus + decoders RIGHT NOW. Without this, the
                        // ExoPlayer instance survives until the JVM GCs
                        // it or onDestroy fires (whichever is later)
                        // and the audio stream keeps playing.
                        p.release();
                    } catch (Exception ignored) { /* already torn down */ }
                    try {
                        session.release();
                    } catch (Exception ignored) { /* idempotent */ }
                    session = null;
                }
                try {
                    stopForeground(STOP_FOREGROUND_REMOVE);
                } catch (Exception ignored) { /* not foreground */ }
                stopSelf();
                // Don't fall through to super.onStartCommand below —
                // returning START_NOT_STICKY ensures the service stays
                // dead even if the system tries to restart it.
                return START_NOT_STICKY;
            } else if (intent.hasExtra(EXTRA_URL)) {
                handlePlay(intent);
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    /** Widget / external "channel up / down" trigger. Routes through the
     *  ChannelAwarePlayer wrapper so the lock-screen path and the widget
     *  path share identical zap semantics (same fast MediaItem swap,
     *  same {@link ChannelStore#setCurrentChannelNumber} update). */
    private void handleZap(int delta) {
        if (session == null) return;
        Player p = session.getPlayer();
        if (p == null) return;
        if (delta > 0) p.seekToNextMediaItem();
        else           p.seekToPreviousMediaItem();
    }

    private void handlePlay(Intent intent) {
        if (session == null) return;
        String url = intent.getStringExtra(EXTRA_URL);
        String userAgent = intent.getStringExtra(EXTRA_USER_AGENT);
        String title = intent.getStringExtra(EXTRA_TITLE);
        boolean isLive = intent.getBooleanExtra(EXTRA_IS_LIVE, true);
        boolean zapOnly = intent.getBooleanExtra(EXTRA_ZAP_ONLY, false);
        String[] hk = intent.getStringArrayExtra(EXTRA_HEADER_KEYS);
        String[] hv = intent.getStringArrayExtra(EXTRA_HEADER_VALS);
        if (url == null || url.isEmpty()) return;

        // Fast zap path: skip the expensive player rebuild and just swap
        // the MediaItem on the existing ExoPlayer. This is what makes
        // channel switching feel instant — the data source factory,
        // load control and audio focus stay warm.
        //
        // session.getPlayer() returns our ChannelAwarePlayer wrapper —
        // calling Player methods on it forwards straight through to the
        // wrapped ExoPlayer, so the warm state is preserved.
        if (zapOnly) {
            Player p = session.getPlayer();
            MediaItem zapItem = buildMediaItem(url, title, isLive);
            p.setMediaItem(zapItem);
            p.setPlayWhenReady(true);
            p.prepare();
            return;
        }

        Map<String, String> headers = new HashMap<>();
        if (hk != null && hv != null) {
            for (int i = 0; i < hk.length && i < hv.length; i++) {
                if (hk[i] != null && hv[i] != null) headers.put(hk[i], hv[i]);
            }
        }

        // Rebuild player with fresh data source factory carrying the
        // caller-supplied UA + extra headers — IPTV portals often need a
        // specific UA per stream.
        ExoPlayer fresh = buildPlayer(
                userAgent != null && !userAgent.isEmpty()
                        ? userAgent
                        : "IPTVSmartersPlayer/3.1.6 (Linux; Android) ExoPlayerLib/2.19.1",
                headers
        );
        fresh.setHandleAudioBecomingNoisy(true);
        fresh.setAudioAttributes(
                new androidx.media3.common.AudioAttributes.Builder()
                        .setContentType(androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE)
                        .setUsage(androidx.media3.common.C.USAGE_MEDIA)
                        .build(),
                true
        );

        // Wrap the new ExoPlayer in the same ChannelAwarePlayer wrapper so
        // lock-screen ⏭ / ⏮ keep working after a full rebuild.
        Player oldWrapped = session.getPlayer();
        ChannelAwarePlayer wrapped = new ChannelAwarePlayer(fresh);
        session.setPlayer(wrapped);
        if (oldWrapped instanceof ChannelAwarePlayer) {
            // Release the *inner* ExoPlayer, not the wrapper (it has no
            // resources of its own). getWrappedPlayer() returns Player;
            // we built it from an ExoPlayer so the cast is safe.
            Player inner = ((ChannelAwarePlayer) oldWrapped).getWrappedPlayer();
            if (inner instanceof ExoPlayer) ((ExoPlayer) inner).release();
        } else if (oldWrapped instanceof ExoPlayer) {
            ((ExoPlayer) oldWrapped).release();
        }

        fresh.setMediaItem(buildMediaItem(url, title, isLive));
        fresh.setPlayWhenReady(true);
        fresh.prepare();
    }

    /** Same builder used internally by {@link #handlePlay} and by
     *  {@link ChannelAwarePlayer} when the lock-screen / widget triggers
     *  a fast channel swap. Public so the wrapper can stay decoupled
     *  from the service's private state. */
    public static MediaItem buildLiveMediaItem(String url, String title) {
        return buildMediaItem(url, title, /* isLive = */ true);
    }

    private static MediaItem buildMediaItem(String url, String title, boolean isLive) {
        MediaItem.Builder mib = new MediaItem.Builder().setUri(Uri.parse(url));
        // BUG FIX (2026-01) — IPTV manifest parsing failures.
        //
        // Many providers hide the stream type behind dynamic query
        // parameters (`/get.php?username=…&type=m3u_plus&output=ts`)
        // or proxied paths (`/live/abc/123/`) with NO recognisable
        // file extension at all. ExoPlayer's DefaultMediaSourceFactory
        // tries to guess the container from the URL string in that
        // case, picks the wrong one (often Progressive), and the
        // manifest parser throws "MalformedSegmentException" or
        // "ParserException: Input does not start with the #EXTM3U
        // header". The user sees a sticky error overlay (see Bug 1
        // fix in PlayerActivity) and the channel never plays.
        //
        // Fix: explicitly tag the MIME so ExoPlayer wires up the right
        // MediaSource.Factory. The match order matters:
        //   1. Lower-cased URL string (path + query) for extension /
        //      query-param hints (`m3u8`, `mpd`, `ts`, `mp4`, `output=hls`,
        //      `format=m3u8`, etc.).
        //   2. Common Xtream / IPTV path conventions
        //      (`/live/`, `/movie/`, `/series/`) — Xtream's "live"
        //      endpoint ALWAYS serves HLS/TS, "movie" and "series"
        //      serve MP4/MKV containers.
        //   3. Fall back to HLS for Live streams (the dominant IPTV
        //      protocol) and Progressive for VOD when nothing matched
        //      — the previous default was no MIME at all, which made
        //      ExoPlayer make worse guesses for IPTV.
        String lower = url == null ? "" : url.toLowerCase();
        String inferredMime = null;
        if (lower.contains(".m3u8") || lower.contains("type=m3u")
                || lower.contains("output=hls") || lower.contains("format=m3u8")
                || lower.contains("format=hls")) {
            inferredMime = MimeTypes.APPLICATION_M3U8;
        } else if (lower.contains(".mpd") || lower.contains("format=mpd")
                || lower.contains("format=dash")) {
            inferredMime = MimeTypes.APPLICATION_MPD;
        } else if (lower.endsWith(".ts") || lower.contains("output=ts")) {
            inferredMime = MimeTypes.VIDEO_MP2T;
        } else if (lower.endsWith(".mp4") || lower.contains("format=mp4")) {
            inferredMime = MimeTypes.APPLICATION_MP4;
        } else if (lower.endsWith(".mkv")) {
            inferredMime = MimeTypes.APPLICATION_MATROSKA;
        }
        if (inferredMime == null) {
            // No explicit hint → use the Xtream URL-path convention as
            // a final disambiguator. /live/ is virtually always HLS;
            // /movie/ + /series/ are pre-recorded VOD containers.
            if (lower.contains("/live/")) {
                inferredMime = MimeTypes.APPLICATION_M3U8;
            } else if (lower.contains("/movie/") || lower.contains("/series/")
                    || lower.contains("/vod/")) {
                inferredMime = MimeTypes.APPLICATION_MP4;
            } else if (isLive) {
                // Pure-IPTV last-resort: live streams without ANY
                // disambiguator are overwhelmingly HLS in practice.
                inferredMime = MimeTypes.APPLICATION_M3U8;
            }
        }
        if (inferredMime != null) {
            mib.setMimeType(inferredMime);
        }
        if (isLive) {
            mib.setLiveConfiguration(
                    new MediaItem.LiveConfiguration.Builder()
                            .setTargetOffsetMs(8000)
                            .build()
            );
        }
        if (title != null && !title.isEmpty()) {
            mib.setMediaMetadata(
                    new MediaMetadata.Builder()
                            .setTitle(title)
                            .setArtist("Nadiplayer")
                            .build()
            );
        }
        return mib.build();
    }

    @OptIn(markerClass = UnstableApi.class)
    private ExoPlayer buildPlayer(String userAgent, Map<String, String> headers) {
        DefaultHttpDataSource.Factory httpFactory = new DefaultHttpDataSource.Factory()
                .setUserAgent(userAgent)
                .setAllowCrossProtocolRedirects(true)
                .setConnectTimeoutMs(15000)
                .setReadTimeoutMs(20000);
        if (!headers.isEmpty()) {
            httpFactory.setDefaultRequestProperties(headers);
        }

        DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
                .setBufferDurationsMs(15000, 60000, 2500, 8000)
                .setPrioritizeTimeOverSizeThresholds(true)
                .build();

        DefaultMediaSourceFactory msFactory = new DefaultMediaSourceFactory(this)
                .setDataSourceFactory(httpFactory)
                .setLiveTargetOffsetMs(8000);

        return new ExoPlayer.Builder(this)
                .setLoadControl(loadControl)
                .setMediaSourceFactory(msFactory)
                .build();
    }

    @Override
    public void onTaskRemoved(@Nullable Intent rootIntent) {
        // If the user swipes the app away AND nothing is playing, stop
        // the service so we don't leak. If something IS playing, keep
        // the foreground notification — that's exactly the "audio while
        // app is killed" UX the user asked for.
        if (session == null) {
            stopSelf();
            return;
        }
        if (!session.getPlayer().getPlayWhenReady() ||
                session.getPlayer().getMediaItemCount() == 0) {
            stopSelf();
        }
    }

    @Override
    public void onDestroy() {
        if (session != null) {
            session.getPlayer().release();
            session.release();
            session = null;
        }
        super.onDestroy();
    }
}
