package com.puntoz.pro;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import androidx.media3.common.MediaItem;
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.hls.HlsMediaSource;
import androidx.media3.exoplayer.hls.DefaultHlsExtractorFactory;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.extractor.ts.DefaultTsPayloadReaderFactory;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView;

import java.io.InputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.ArrayList;
import java.util.HashMap;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URLEncoder;

@UnstableApi
public class PlayerActivity extends Activity {

    PlayerView playerView;
    ExoPlayer player;
    Handler handler = new Handler();

    FrameLayout root;
    LinearLayout osdBox;
    ImageView logoView;
    TextView nowView;
    TextView nowTimeView;
    TextView nextView;
    TextView nextTimeView;
    TextView remainView;
    View progressDone;
    View progressLeft;

    LinearLayout guideMenu;
    LinearLayout catMenu;
    int menuState = 0; // 0 cerrado, 1 guia, 2 categorias
    int focusedChannelIndex = 0;
    int playingChannelIndex = 0;
    int focusedCategoryIndex = 0;
    int catActiveColumn = 0; // 0 categorias, 1 canales
    int maxVisibleCats = 18;
    int maxVisibleChannels = 8;
    ArrayList<CategoryItem> categoryItems = new ArrayList<>();
    ArrayList<ChannelItem> playingCategoryChannels = new ArrayList<>();

    String url = "";
    String channelName = "Puntoz TV";
    String channelLogo = "";
    String nowTitle = "No information";
    String nextTitle = "No information";
    String startTime = "";
    String endTime = "";
    String nextStartTime = "";
    String nextEndTime = "";
    String streamId = "";
    String channelsJson = "";
    String catName = "PRUEBA PUNTOZ 2026";
    String playingCatName = "Canales";

    ArrayList<EpgItem> epgItems = new ArrayList<>();
    ArrayList<ChannelItem> channelItems = new ArrayList<>();
    ArrayList<ChannelItem> playingChannelItems = new ArrayList<>();
    Runnable epgTicker;
    long lastMenuRebuildMs = 0;
    boolean menuRebuildPending = false;
    long lastChannelMoveMs = 0;
    long lastCategoryMoveMs = 0;
    long blockEnterUntilMs = 0;
    String pendingCategoryId = "";
    Runnable pendingCategoryLoadRunnable = null;
    HashMap<String, Bitmap> logoCache = new HashMap<>();
    HashMap<String, ArrayList<ChannelItem>> canalesCache = new HashMap<>();
    HashMap<String, String> catNameCache = new HashMap<>();
    Runnable osdHideTicker;
    boolean epgReloading = false;
    long lastEpgReload = 0;
    boolean focusedEpgLoading = false;
    boolean preloadEpgLoading = false;
    boolean preloadCategoriaLoading = false;
    String lastFocusedEpgSid = "";

    int amarillo = Color.rgb(255, 220, 0);
    int azul = Color.rgb(0, 210, 255);

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        url = getIntent().getStringExtra("url");
        channelName = getIntent().getStringExtra("channel_name");
        channelLogo = getIntent().getStringExtra("channel_logo");
        nowTitle = getIntent().getStringExtra("now_title");
        nextTitle = getIntent().getStringExtra("next_title");
        startTime = getIntent().getStringExtra("start_time");
        endTime = getIntent().getStringExtra("end_time");
        nextStartTime = getIntent().getStringExtra("next_start_time");
        nextEndTime = getIntent().getStringExtra("next_end_time");
        streamId = getIntent().getStringExtra("stream_id");
        channelsJson = getIntent().getStringExtra("channels_json");
        catName = getIntent().getStringExtra("cat_name");

        if (url == null) url = "";
        if (channelName == null || channelName.trim().isEmpty()) channelName = "Puntoz TV";
        if (channelLogo == null) channelLogo = "";
        if (nowTitle == null || nowTitle.trim().isEmpty()) nowTitle = "No information";
        if (nextTitle == null || nextTitle.trim().isEmpty()) nextTitle = "No information";
        if (startTime == null) startTime = "";
        if (endTime == null) endTime = "";
        if (nextStartTime == null) nextStartTime = "";
        if (nextEndTime == null) nextEndTime = "";
        if (streamId == null) streamId = "";
        if (channelsJson == null) channelsJson = "";
        if (catName == null || catName.trim().isEmpty()) catName = "Canales";
        playingCatName = catName;

        /*
         * PZ_PLAYER_VIDEO_PRIMERO:
         * Cargar solo canales del payload primero.
         * Menús/categorías pesadas se cargan después del arranque del video.
         */
        cargarCanalesDesdeJson();

        for (int i = 0; i < channelItems.size(); i++) {
            if (channelItems.get(i).sid.equals(streamId)) {
                focusedChannelIndex = i;
                playingChannelIndex = i;
                break;
            }
        }

        root = new FrameLayout(this);

        playerView = new PlayerView(this);
        playerView.setUseController(false);
        playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);

        root.addView(playerView, new FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT
        ));

        crearOSD();

        setContentView(root);

        DefaultTrackSelector selector = new DefaultTrackSelector(this);
        selector.setParameters(selector.buildUponParameters()
                .setPreferredAudioLanguage("es")
                .setPreferredTextLanguage("es")
        );

        /*
         * PZ_BUFFER_IPTV_RAPIDO:
         * Menor buffer inicial para abrir canales IPTV mucho más rápido.
         */
        DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
                .setBufferDurationsMs(1000, 5000, 250, 500)
                .build();

        player = new ExoPlayer.Builder(this)
                .setTrackSelector(selector)
                .setLoadControl(loadControl)
                .build();

        playerView.setPlayer(player);
        playUrl(url);

        /*
         * Carga secundaria después de iniciar reproducción.
         */
        handler.postDelayed(() -> {
            try { cargarMenuAndroid(null); } catch (Exception ignored) {}
            try { precargarCategoriasEnRam(); } catch (Exception ignored) {}
        }, 1500);
    }

    private TextView tv(String text, int size, int color) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextColor(color);
        t.setTextSize(size);
        t.setSingleLine(true);
        t.setMaxLines(1);
        t.setEllipsize(android.text.TextUtils.TruncateAt.END);
        t.setIncludeFontPadding(false);
        return t;
    }

    private SpannableString labelColor(String label, String text, int color) {
        SpannableString sp = new SpannableString(label + text);
        sp.setSpan(new ForegroundColorSpan(color), 0, label.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        return sp;
    }

    private TextView chip(String txt) {
        TextView t = tv(txt, 13, Color.LTGRAY);
        t.setGravity(Gravity.CENTER);
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.argb(35, 0, 0, 0));
        bg.setStroke(2, Color.argb(160, 180, 180, 180));
        bg.setCornerRadius(8);
        t.setBackground(bg);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(68, 30);
        lp.setMargins(8, 0, 8, 0);
        t.setLayoutParams(lp);
        return t;
    }

    private void crearOSD() {
        osdBox = new LinearLayout(this);
        osdBox.setOrientation(LinearLayout.HORIZONTAL);
        osdBox.setGravity(Gravity.CENTER_VERTICAL);
        osdBox.setPadding(28, 16, 28, 16);
        osdBox.setVisibility(View.GONE);

        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.argb(82, 0, 0, 0));
        bg.setStroke(2, Color.argb(80, 255, 255, 255));
        bg.setCornerRadius(16);
        osdBox.setBackground(bg);

        logoView = new ImageView(this);
        logoView.setScaleType(ImageView.ScaleType.FIT_CENTER);
        LinearLayout.LayoutParams logoLp = new LinearLayout.LayoutParams(220, 150);
        logoLp.setMargins(0, 0, 28, 0);
        osdBox.addView(logoView, logoLp);

        LinearLayout info = new LinearLayout(this);
        info.setOrientation(LinearLayout.VERTICAL);
        info.setPadding(0, 18, 0, 0);

        nowView = tv("", 13, Color.WHITE);
        nowView.setText(labelColor("Ahora: ", nowTitle, amarillo));

        nowTimeView = tv(formatoHorario(startTime, endTime), 11, Color.WHITE);

        LinearLayout progressRow = new LinearLayout(this);
        progressRow.setOrientation(LinearLayout.HORIZONTAL);
        progressRow.setGravity(Gravity.CENTER_VERTICAL);

        LinearLayout bar = new LinearLayout(this);
        bar.setOrientation(LinearLayout.HORIZONTAL);
        GradientDrawable barBg = new GradientDrawable();
        barBg.setColor(Color.argb(120, 160, 170, 180));
        barBg.setCornerRadius(6);
        bar.setBackground(barBg);

        int progress = calcularProgreso(startTime, endTime);

        progressDone = new View(this);
        progressDone.setBackgroundColor(amarillo);
        bar.addView(progressDone, new LinearLayout.LayoutParams(0, 7, progress));

        progressLeft = new View(this);
        progressLeft.setBackgroundColor(Color.TRANSPARENT);
        bar.addView(progressLeft, new LinearLayout.LayoutParams(0, 7, Math.max(1, 100 - progress)));

        LinearLayout.LayoutParams barLp = new LinearLayout.LayoutParams(560, 7);
        barLp.setMargins(0, 1, 14, 1);
        progressRow.addView(bar, barLp);

        remainView = tv(minutosRestantes(endTime), 11, Color.WHITE);
        progressRow.addView(remainView);

        nextView = tv("", 13, Color.WHITE);
        nextView.setText(labelColor("Sigue: ", nextTitle, azul));

        nextTimeView = tv(formatoHorario(nextStartTime, nextEndTime), 12, Color.WHITE);

        info.addView(nowView);
        info.addView(nowTimeView);
        info.addView(progressRow);
        info.addView(nextView);
        info.addView(nextTimeView);

        osdBox.addView(info, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1));

        View divider = new View(this);
        divider.setBackgroundColor(Color.argb(130, 180, 180, 180));
        LinearLayout.LayoutParams divLp = new LinearLayout.LayoutParams(2, 128);
        divLp.setMargins(35, 0, 35, 0);
        osdBox.addView(divider, divLp);

        LinearLayout right = new LinearLayout(this);
        right.setOrientation(LinearLayout.HORIZONTAL);
        right.setGravity(Gravity.CENTER);
        right.addView(chip("FHD"));
        right.addView(chip("16:9"));
        right.addView(chip("STEREO"));

        osdBox.addView(right, new LinearLayout.LayoutParams(280, LinearLayout.LayoutParams.WRAP_CONTENT));

        FrameLayout.LayoutParams osdParams = new FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.WRAP_CONTENT
        );
        osdParams.gravity = Gravity.BOTTOM;
        osdParams.setMargins(70, 0, 70, 42);

        root.addView(osdBox, osdParams);

        crearMenusLaterales();

        actualizarEpgViews();
        cargarLogo(channelLogo);

        cargarEpgUnaVez();
        iniciarEpgLocal();
    }



    private GradientDrawable fondoMenu(int alpha) {
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.argb(alpha, 5, 10, 14));
        bg.setStroke(2, Color.argb(95, 255, 255, 255));
        bg.setCornerRadius(18);
        return bg;
    }

    private TextView itemMenu(String txt, boolean foco, int size) {
        TextView t = tv(txt, size, Color.WHITE);
        t.setGravity(Gravity.CENTER_VERTICAL);
        t.setPadding(18, 8, 18, 8);

        if (foco) {
            GradientDrawable bg = new GradientDrawable();
            bg.setColor(Color.argb(120, 255, 220, 0));
            bg.setStroke(2, Color.argb(230, 255, 220, 0));
            bg.setCornerRadius(12);
            t.setBackground(bg);
            t.setTextColor(Color.WHITE);
        }

        return t;
    }

    private void crearMenusLaterales() {
        guideMenu = new LinearLayout(this);
        guideMenu.setOrientation(LinearLayout.HORIZONTAL);
        guideMenu.setPadding(18, 18, 18, 18);
        guideMenu.setVisibility(View.GONE);
        guideMenu.setBackground(fondoMenu(70));

        LinearLayout colCanales = new LinearLayout(this);
        colCanales.setOrientation(LinearLayout.VERTICAL);
        TextView h1 = itemMenu(catName, true, 18);
        colCanales.addView(h1);

        if (channelItems.size() > 0) {
            int chStart = Math.max(0, focusedChannelIndex - (maxVisibleChannels / 2));
            if (chStart + maxVisibleChannels > channelItems.size()) chStart = Math.max(0, channelItems.size() - maxVisibleChannels);

            for (int i = chStart; i < channelItems.size() && i < chStart + maxVisibleChannels; i++) {
                ChannelItem ch = channelItems.get(i);
                boolean foco = i == focusedChannelIndex;
                colCanales.addView(canalItem(ch, foco));
            }
        } else {
            for (int i = 1; i <= 9; i++) {
                String txt = (i == 1 ? "▶ " : "   ") + (i == 1 ? channelName : "Canal " + i);
                colCanales.addView(itemMenu(txt, i == 1, 17));
            }
        }

        ChannelItem focoCh = canalEnFoco();

        String guiaNombre = channelName;
        String guiaNow = nowTitle;
        String guiaNext = nextTitle;
        String guiaSt = startTime;
        String guiaEn = endTime;
        String guiaNs = nextStartTime;
        String guiaNe = nextEndTime;

        if (focoCh != null) {
            guiaNombre = focoCh.name;
            guiaNow = (focoCh.now == null || focoCh.now.trim().isEmpty()) ? "Programación en Actualización" : focoCh.now;
            guiaNext = (focoCh.next == null || focoCh.next.trim().isEmpty()) ? "Programación en Actualización" : focoCh.next;
            guiaSt = focoCh.st == null ? "" : focoCh.st;
            guiaEn = focoCh.en == null ? "" : focoCh.en;
            guiaNs = focoCh.ns == null ? "" : focoCh.ns;
            guiaNe = focoCh.ne == null ? "" : focoCh.ne;
        }

        String detalleTitulo = guiaNow;
        String detalleDesc = "";
        long detalleStart = 0;
        long detalleEnd = 0;
        int detalleIndex = -1;

        if (focoCh != null && focoCh.programs != null && focoCh.programs.size() > 0) {
            long nowTs = System.currentTimeMillis() / 1000L;
            for (int i = 0; i < focoCh.programs.size(); i++) {
                EpgItem ep = focoCh.programs.get(i);
                if (nowTs >= ep.start && nowTs < ep.stop) {
                    detalleIndex = i;
                    break;
                }
            }
            if (detalleIndex < 0) detalleIndex = 0;

            EpgItem det = focoCh.programs.get(detalleIndex);
            detalleTitulo = det.title == null ? guiaNow : det.title;
            detalleDesc = det.desc == null ? "" : det.desc;
            detalleStart = det.start;
            detalleEnd = det.stop;
        } else {
            try { detalleStart = Long.parseLong(guiaSt); } catch(Exception ignored) {}
            try { detalleEnd = Long.parseLong(guiaEn); } catch(Exception ignored) {}
        }

        LinearLayout colProg = new LinearLayout(this);
        colProg.setOrientation(LinearLayout.VERTICAL);

        LinearLayout headProg = new LinearLayout(this);
        headProg.setOrientation(LinearLayout.HORIZONTAL);
        headProg.setPadding(34, 0, 0, 8);

        ImageView logoProg = new ImageView(this);
        logoProg.setScaleType(ImageView.ScaleType.FIT_CENTER);
        if (focoCh != null) cargarLogoEn(logoProg, focoCh.logo);
        headProg.addView(logoProg, new LinearLayout.LayoutParams(116, 116));

        LinearLayout headText = new LinearLayout(this);
        headText.setOrientation(LinearLayout.VERTICAL);
        headText.setPadding(16, 4, 0, 0);
        headText.addView(itemMenu(guiaNombre, false, 16));
        headText.addView(itemMenu(catName, false, 13));
        headProg.addView(headText, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1));

        colProg.addView(headProg);

        if (focoCh != null && focoCh.programs != null && focoCh.programs.size() > 0) {
            int maxProg = Math.min(20, focoCh.programs.size());

            for (int i = 0; i < maxProg; i++) {
                EpgItem ep = focoCh.programs.get(i);
                boolean actual = (i == detalleIndex);

                String linea = horaBonita(String.valueOf(ep.start)) + "   " + ep.title;

                TextView fila = itemMenu(linea, false, 11);

                if (actual) {
                    fila.setTextColor(Color.YELLOW);
                }

                colProg.addView(fila);
            }
        } else {
            colProg.addView(itemMenu(horaBonita(guiaSt) + "   " + guiaNow, true, 12));
            colProg.addView(itemMenu(horaBonita(guiaNs) + "   " + guiaNext, false, 12));
        }

        LinearLayout colDetalle = new LinearLayout(this);
        colDetalle.setOrientation(LinearLayout.VERTICAL);
        colDetalle.setPadding(24, 24, 24, 24);
        colDetalle.setBackground(fondoMenu(88));

        TextView detTitulo = new TextView(this);
        detTitulo.setText(detalleTitulo);
        detTitulo.setTextColor(Color.WHITE);
        detTitulo.setTextSize(18);
        detTitulo.setTypeface(null, android.graphics.Typeface.BOLD);
        detTitulo.setMaxLines(2);
        detTitulo.setEllipsize(android.text.TextUtils.TruncateAt.END);
        colDetalle.addView(detTitulo);

        TextView detHorario = new TextView(this);
        detHorario.setText(formatoHorario(String.valueOf(detalleStart), String.valueOf(detalleEnd)));
        detHorario.setTextColor(Color.WHITE);
        detHorario.setTextSize(14);
        detHorario.setPadding(0, 18, 0, 8);
        colDetalle.addView(detHorario);

        int pctProg = 0;
        try {
            long nowL = System.currentTimeMillis() / 1000L;
            if (detalleEnd > detalleStart) {
                pctProg = (int)(((nowL - detalleStart) * 100L) / (detalleEnd - detalleStart));
                if (pctProg < 0) pctProg = 0;
                if (pctProg > 100) pctProg = 100;
            }
        } catch(Exception ignored) {}

        LinearLayout barraTiempo = new LinearLayout(this);
        barraTiempo.setOrientation(LinearLayout.HORIZONTAL);
        barraTiempo.setPadding(0, 8, 0, 8);

        View barraLlena = new View(this);
        barraLlena.setBackgroundColor(Color.argb(235, 220, 225, 80));

        View barraVacia = new View(this);
        barraVacia.setBackgroundColor(Color.argb(130, 230, 230, 230));

        barraTiempo.addView(barraLlena, new LinearLayout.LayoutParams(0, 8, Math.max(1, pctProg)));
        barraTiempo.addView(barraVacia, new LinearLayout.LayoutParams(0, 8, Math.max(1, 100 - pctProg)));

        colDetalle.addView(barraTiempo);

        TextView detMin = new TextView(this);
        detMin.setText(minutosRestantes(String.valueOf(detalleEnd)));
        detMin.setTextColor(Color.WHITE);
        detMin.setTextSize(13);
        detMin.setPadding(0, 4, 0, 16);
        colDetalle.addView(detMin);

        TextView detCanal = new TextView(this);
        detCanal.setText("Canal: " + guiaNombre);
        detCanal.setTextColor(Color.WHITE);
        detCanal.setTextSize(13);
        detCanal.setPadding(0, 0, 0, 18);
        colDetalle.addView(detCanal);

        TextView detDesc = new TextView(this);
        detDesc.setText(detalleDesc.trim().isEmpty() ? "Sin sinopsis disponible para este programa." : detalleDesc);
        detDesc.setTextColor(Color.WHITE);
        detDesc.setTextSize(13);
        detDesc.setMaxLines(8);
        detDesc.setEllipsize(android.text.TextUtils.TruncateAt.END);
        colDetalle.addView(detDesc);

        guideMenu.addView(colCanales, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 38));

        View sepGuide1 = new View(this);
        sepGuide1.setBackgroundColor(Color.argb(55, 0, 210, 255));
        guideMenu.addView(sepGuide1, new LinearLayout.LayoutParams(10, LinearLayout.LayoutParams.MATCH_PARENT));

        guideMenu.addView(colProg, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 34));

        View sepGuide2 = new View(this);
        sepGuide2.setBackgroundColor(Color.argb(55, 0, 210, 255));
        guideMenu.addView(sepGuide2, new LinearLayout.LayoutParams(10, LinearLayout.LayoutParams.MATCH_PARENT));

        LinearLayout.LayoutParams detalleParams = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 28);
        detalleParams.setMargins(16, 48, 0, 48);
        guideMenu.addView(colDetalle, detalleParams);

        FrameLayout.LayoutParams gp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
        gp.gravity = Gravity.LEFT;
        gp.setMargins(24, 32, 24, 34);
        root.addView(guideMenu, gp);

        catMenu = new LinearLayout(this);
        catMenu.setOrientation(LinearLayout.HORIZONTAL);
        catMenu.setPadding(18, 18, 18, 18);
        catMenu.setVisibility(View.GONE);
        catMenu.setBackground(fondoMenu(70));

        LinearLayout colCats = new LinearLayout(this);
        colCats.setOrientation(LinearLayout.VERTICAL);
        colCats.addView(itemMenu("CATEGORIAS", catActiveColumn == 0, 18));
        if (categoryItems.size() > 0) {
            int catStart = Math.max(0, focusedCategoryIndex - (maxVisibleCats / 2));
            if (catStart + maxVisibleCats > categoryItems.size()) catStart = Math.max(0, categoryItems.size() - maxVisibleCats);

            for (int i = catStart; i < categoryItems.size() && i < catStart + maxVisibleCats; i++) {
                boolean focoCat = (catActiveColumn == 0 && i == focusedCategoryIndex);
                colCats.addView(itemMenu("TEST-" + categoryItems.get(i).name, focoCat, 16));
            }
        } else {
            colCats.addView(itemMenu("   Cargando categorías...", false, 16));
        }

        LinearLayout colCatChannels = new LinearLayout(this);
        colCatChannels.setOrientation(LinearLayout.VERTICAL);
        colCatChannels.addView(itemMenu(catName, catActiveColumn == 1, 18));
        if (channelItems.size() > 0) {
            int chCatStart = Math.max(0, focusedChannelIndex - (maxVisibleChannels / 2));
            if (chCatStart + maxVisibleChannels > channelItems.size()) chCatStart = Math.max(0, channelItems.size() - maxVisibleChannels);

            for (int i = chCatStart; i < channelItems.size() && i < chCatStart + maxVisibleChannels; i++) {
                ChannelItem ch = channelItems.get(i);
                boolean foco = (catActiveColumn == 1 && i == focusedChannelIndex);
                colCatChannels.addView(canalItem(ch, foco));
            }
        } else {
            colCatChannels.addView(itemMenu("Cargando " + catName + "...", true, 17));
        }

        catMenu.addView(colCats, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 42));

        View sepCat = new View(this);
        sepCat.setBackgroundColor(Color.argb(55, 0, 210, 255));
        catMenu.addView(sepCat, new LinearLayout.LayoutParams(10, LinearLayout.LayoutParams.MATCH_PARENT));

        catMenu.addView(colCatChannels, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 68));

        FrameLayout.LayoutParams cp = new FrameLayout.LayoutParams(1120, FrameLayout.LayoutParams.MATCH_PARENT);
        cp.gravity = Gravity.LEFT;
        cp.setMargins(24, 32, 0, 34);
        root.addView(catMenu, cp);
    }


    private ArrayList<ChannelItem> copiarCanales(ArrayList<ChannelItem> src) {
        ArrayList<ChannelItem> out = new ArrayList<>();
        if (src == null) return out;

        for (ChannelItem a : src) {
            ChannelItem b = new ChannelItem();
            b.url = a.url;
            b.name = a.name;
            b.logo = a.logo;
            b.sid = a.sid;
            b.now = a.now;
            b.next = a.next;
            b.st = a.st;
            b.en = a.en;
            b.ns = a.ns;
            b.ne = a.ne;
            b.programs = new ArrayList<>(a.programs);
            out.add(b);
        }
        return out;
    }

    private void guardarListaRealReproduccion() {
        playingChannelItems = copiarCanales(channelItems);
        playingCategoryChannels = copiarCanales(channelItems);
        playingCatName = catName;
        for (int i = 0; i < channelItems.size(); i++) {
            if (channelItems.get(i).sid.equals(streamId)) {
                playingChannelIndex = i;
                focusedChannelIndex = i;
                break;
            }
        }
    }

    private void restaurarListaRealReproduccion() {
        if (playingChannelItems != null && playingChannelItems.size() > 0) {
            channelItems = copiarCanales(playingChannelItems);
        }

        catName = playingCatName;

        if (playingChannelIndex < 0) playingChannelIndex = 0;
        if (playingChannelIndex >= channelItems.size()) playingChannelIndex = 0;

        focusedChannelIndex = playingChannelIndex;
    }


    private void mostrarGuiaMenu() {
        menuState = 1;
        restaurarListaRealReproduccion();
        lastFocusedEpgSid = "";
        reconstruirMenus();
        if (catMenu != null) catMenu.setVisibility(View.GONE);
        if (guideMenu != null) guideMenu.setVisibility(View.VISIBLE);
        if (osdBox != null) osdBox.setVisibility(View.GONE);
    }

    private void mostrarCategoriasMenu() {
        menuState = 2;
        catActiveColumn = 0;
        lastFocusedEpgSid = "";
        reconstruirMenus();
        if (guideMenu != null) guideMenu.setVisibility(View.GONE);
        if (catMenu != null) catMenu.setVisibility(View.VISIBLE);
        if (osdBox != null) osdBox.setVisibility(View.GONE);
    }


    private void reproducirCanalEnFoco() {
        ChannelItem ch = canalEnFoco();
        if (ch == null || ch.url == null || !ch.url.startsWith("http")) return;

        url = ch.url;
        channelName = ch.name;
        channelLogo = ch.logo;
        streamId = ch.sid;

        nowTitle = (ch.now == null || ch.now.trim().isEmpty()) ? "No information" : ch.now;
        nextTitle = (ch.next == null || ch.next.trim().isEmpty()) ? "No information" : ch.next;
        startTime = ch.st == null ? "" : ch.st;
        endTime = ch.en == null ? "" : ch.en;
        nextStartTime = ch.ns == null ? "" : ch.ns;
        nextEndTime = ch.ne == null ? "" : ch.ne;

        for (int i = 0; i < channelItems.size(); i++) {
            if (channelItems.get(i).sid.equals(streamId)) {
                playingChannelIndex = i;
                focusedChannelIndex = i;
                playingCatName = catName;
                break;
            }
        }

        guardarListaRealReproduccion();

        epgItems.clear();
        cerrarMenus();

        cargarLogo(channelLogo);
        actualizarEpgViews();
        cargarEpgUnaVez();

        try {
            player.stop();
            playUrl(url);
        } catch(Exception ignored) {}
    }


    private void cerrarMenus() {
        menuState = 0;
        restaurarListaRealReproduccion();
        if (guideMenu != null) guideMenu.setVisibility(View.GONE);
        if (catMenu != null) catMenu.setVisibility(View.GONE);
    }




    private static class CategoryItem {
        String id = "";
        String name = "";
    }


    private String cacheKeyCat(String catId) {
        if (catId == null || catId.trim().isEmpty()) return "__HOME__";
        return catId.trim();
    }

    private boolean aplicarCacheCategoria(String catId) {
        String key = cacheKeyCat(catId);

        if (!canalesCache.containsKey(key)) return false;

        channelItems = copiarCanales(canalesCache.get(key));
        catName = catNameCache.containsKey(key) ? catNameCache.get(key) : catName;

        boolean foundPlaying = false;
        for (int i = 0; i < channelItems.size(); i++) {
            if (channelItems.get(i).sid.equals(streamId)) {
                playingChannelIndex = i;
                focusedChannelIndex = i;
                foundPlaying = true;
                break;
            }
        }

        if (!foundPlaying) focusedChannelIndex = 0;

        reconstruirMenus();

        if (menuState == 1 && guideMenu != null) guideMenu.setVisibility(View.VISIBLE);
        if (menuState == 2 && catMenu != null) catMenu.setVisibility(View.VISIBLE);

        lastFocusedEpgSid = "";

        return true;
    }


    private void cargarMenuAndroid(String catId) {
        final String requestCatId = catId;
        if (catId != null && catId.trim().length() > 0 && !catId.equals(pendingCategoryId)) return;
        // if (aplicarCacheCategoria(catId)) return;

        new Thread(() -> {
            try {
                String api = "http://216.152.155.250/PUNTOZ_PANEL/tv_clean.php?android_menu=1";
                if (catId != null && catId.trim().length() > 0) {
                    api += "&cat=" + URLEncoder.encode(catId, "UTF-8");
                }

                URL u = new URL(api);
                HttpURLConnection c = (HttpURLConnection) u.openConnection();
                c.setConnectTimeout(6000);
                c.setReadTimeout(12000);
                c.setRequestProperty("User-Agent", "Puntoz-TV-Android");
                InputStream is = c.getInputStream();

                byte[] buf = new byte[32768];
                StringBuilder sb = new StringBuilder();
                int n;
                while ((n = is.read(buf)) > 0) sb.append(new String(buf, 0, n, "UTF-8"));
                is.close();

                JSONObject json = new JSONObject(sb.toString());

                JSONArray cats = json.optJSONArray("categories");
                if (cats != null) {
                    ArrayList<CategoryItem> tmpCats = new ArrayList<>();
                    for (int i = 0; i < cats.length(); i++) {
                        JSONObject o = cats.getJSONObject(i);
                        CategoryItem ci = new CategoryItem();
                        ci.id = o.optString("id", "");
                        ci.name = o.optString("name", "Sin nombre");
                        if (ci.id.length() > 0) tmpCats.add(ci);
                    }
                    categoryItems = tmpCats;
                }

                JSONArray chans = json.optJSONArray("channels");
                if (chans != null) {
                    ArrayList<ChannelItem> tmpCh = new ArrayList<>();
                    for (int i = 0; i < chans.length(); i++) {
                        JSONObject o = chans.getJSONObject(i);
                        ChannelItem ch = new ChannelItem();
                        ch.url = o.optString("url", "");
                        ch.name = o.optString("name", "Canal");
                        ch.logo = o.optString("logo", "");
                        ch.sid = o.optString("sid", "");
                        ch.now = o.optString("now", "");
                        ch.next = o.optString("next", "");
                        ch.st = o.optString("st", "");
                        ch.en = o.optString("en", "");
                        ch.ns = o.optString("ns", "");
                        ch.ne = o.optString("ne", "");

                        JSONArray arrP = o.optJSONArray("programs");
                        if (arrP != null) {
                            ArrayList<EpgItem> tmpPrograms = new ArrayList<>();
                            for (int pp = 0; pp < arrP.length(); pp++) {
                                JSONObject po = arrP.getJSONObject(pp);
                                EpgItem ei = new EpgItem();
                                ei.title = po.optString("title", "Programación");
                                ei.desc = po.optString("desc", "");
                                ei.start = po.optLong("start", 0);
                                ei.stop = po.optLong("end", po.optLong("stop", 0));
                                if (ei.start > 0 && ei.stop > ei.start) tmpPrograms.add(ei);
                            }
                            ch.programs = tmpPrograms;
                        }

                        if (ch.url.startsWith("http")) tmpCh.add(ch);
                    }
                    channelItems = tmpCh;

                    String keyCache = cacheKeyCat(catId);
                    canalesCache.put(keyCache, copiarCanales(channelItems));

                    boolean foundPlaying = false;
                    for (int k = 0; k < channelItems.size(); k++) {
                        if (channelItems.get(k).sid.equals(streamId)) {
                            playingChannelIndex = k;
                            focusedChannelIndex = k;
                            foundPlaying = true;
                            break;
                        }
                    }

                    if (!foundPlaying && focusedChannelIndex >= channelItems.size()) {
                        focusedChannelIndex = 0;
                    }
                }

                catName = json.optString("cat_name", catName);
                catNameCache.put(cacheKeyCat(catId), catName);

                runOnUiThread(() -> {
                    if (requestCatId != null && requestCatId.trim().length() > 0 && !requestCatId.equals(pendingCategoryId)) return;
                    reconstruirMenus();
                    if (menuState == 1 && guideMenu != null) guideMenu.setVisibility(View.VISIBLE);
                    if (menuState == 2 && catMenu != null) catMenu.setVisibility(View.VISIBLE);
                    if (requestCatId == null || requestCatId.trim().isEmpty()) {
                        precargarCategoriasEnRam();
                    }
                });

            } catch(Exception ignored) {}
        }).start();
    }


    private void precargarCategoriasEnRam() {
        new Thread(() -> {
            try {
                Thread.sleep(1200);

                ArrayList<CategoryItem> catsCopy = new ArrayList<>(categoryItems);

                for (CategoryItem ci : catsCopy) {
                    if (ci == null || ci.id == null || ci.id.trim().isEmpty()) continue;

                    String key = cacheKeyCat(ci.id);
                    if (canalesCache.containsKey(key)) continue;

                    try {
                        String api = "http://216.152.155.250/PUNTOZ_PANEL/tv_clean.php?android_menu=1&cat=" +
                                URLEncoder.encode(ci.id, "UTF-8");

                        URL u = new URL(api);
                        HttpURLConnection c = (HttpURLConnection) u.openConnection();
                        c.setConnectTimeout(3500);
                        c.setReadTimeout(5000);
                        c.setRequestProperty("User-Agent", "Puntoz-TV-Android-PRELOAD");

                        InputStream is = c.getInputStream();
                        byte[] buf = new byte[32768];
                        StringBuilder sb = new StringBuilder();
                        int n;
                        while ((n = is.read(buf)) > 0) sb.append(new String(buf, 0, n, "UTF-8"));
                        is.close();

                        JSONObject json = new JSONObject(sb.toString());
                        JSONArray chans = json.optJSONArray("channels");
                        if (chans == null) continue;

                        ArrayList<ChannelItem> tmpCh = new ArrayList<>();

                        for (int i = 0; i < chans.length(); i++) {
                            JSONObject o = chans.getJSONObject(i);
                            ChannelItem ch = new ChannelItem();
                            ch.url = o.optString("url", "");
                            ch.name = o.optString("name", "Canal");
                            ch.logo = o.optString("logo", "");
                            ch.sid = o.optString("sid", "");
                            ch.now = o.optString("now", "");
                            ch.next = o.optString("next", "");
                            ch.st = o.optString("st", "");
                            ch.en = o.optString("en", "");
                            ch.ns = o.optString("ns", "");
                            ch.ne = o.optString("ne", "");

                            JSONArray arrP = o.optJSONArray("programs");
                            if (arrP != null) {
                                ArrayList<EpgItem> tmpPrograms = new ArrayList<>();
                                for (int pp = 0; pp < arrP.length(); pp++) {
                                    JSONObject po = arrP.getJSONObject(pp);
                                    EpgItem ei = new EpgItem();
                                    ei.title = po.optString("title", "Programación");
                                    ei.desc = po.optString("desc", "");
                                    ei.start = po.optLong("start", 0);
                                    ei.stop = po.optLong("end", po.optLong("stop", 0));
                                    if (ei.start > 0 && ei.stop > ei.start) tmpPrograms.add(ei);
                                }
                                ch.programs = tmpPrograms;
                            }

                            if (ch.url.startsWith("http")) tmpCh.add(ch);
                        }

                        canalesCache.put(key, copiarCanales(tmpCh));
                        catNameCache.put(key, json.optString("cat_name", ci.name));

                    } catch(Exception ignored) {}
                }

            } catch(Exception ignored) {}
        }).start();
    }

    private CategoryItem categoriaEnFoco() {
        if (categoryItems == null || categoryItems.size() == 0) return null;
        if (focusedCategoryIndex < 0) focusedCategoryIndex = 0;
        if (focusedCategoryIndex >= categoryItems.size()) focusedCategoryIndex = categoryItems.size() - 1;
        return categoryItems.get(focusedCategoryIndex);
    }

    private void cargarCategoriaEnFoco() {
        CategoryItem ci = categoriaEnFoco();
        if (ci == null) return;
        catName = ci.name;
        focusedChannelIndex = 0;
        lastFocusedEpgSid = "";
        cargarMenuAndroid(ci.id);
    }


    private static class ChannelItem {
        String url = "";
        String name = "";
        String logo = "";
        String sid = "";
        String now = "";
        String next = "";
        String st = "";
        String en = "";
        String ns = "";
        String ne = "";
        ArrayList<EpgItem> programs = new ArrayList<>();
    }

    private void cargarCanalesDesdeJson() {
        try {
            channelItems.clear();

            if (channelsJson == null || channelsJson.trim().isEmpty()) return;

            JSONArray arr = new JSONArray(channelsJson);
            for (int i = 0; i < arr.length(); i++) {
                JSONObject o = arr.getJSONObject(i);
                ChannelItem c = new ChannelItem();
                c.url = o.optString("url", "");
                c.name = o.optString("name", "Canal");
                c.logo = o.optString("logo", "");
                c.sid = o.optString("sid", "");
                c.now = o.optString("now", "");
                c.next = o.optString("next", "");
                c.st = o.optString("st", "");
                c.en = o.optString("en", "");
                c.ns = o.optString("ns", "");
                c.ne = o.optString("ne", "");
                if (c.url.startsWith("http")) channelItems.add(c);
            }
        } catch(Exception ignored) {}
    }




    private void cargarLogoEn(ImageView img, String logoUrl) {
        if (img == null || logoUrl == null || !logoUrl.startsWith("http")) return;

        Bitmap cached = logoCache.get(logoUrl);
        if (cached != null) {
            img.setImageBitmap(cached);
            return;
        }

        new Thread(() -> {
            try {
                InputStream in = new URL(logoUrl).openStream();
                Bitmap bmp = BitmapFactory.decodeStream(in);
                logoCache.put(logoUrl, bmp);
                runOnUiThread(() -> img.setImageBitmap(bmp));
            } catch (Exception ignored) {}
        }).start();
    }

    private LinearLayout barraProgresoCanal(String st, String en) {
        LinearLayout bar = new LinearLayout(this);
        bar.setOrientation(LinearLayout.HORIZONTAL);

        int pr = calcularProgreso(st == null ? "" : st, en == null ? "" : en);

        View done = new View(this);
        done.setBackgroundColor(Color.argb(210, 190, 190, 190));
        bar.addView(done, new LinearLayout.LayoutParams(0, 6, Math.max(1, pr)));

        View left = new View(this);
        left.setBackgroundColor(Color.argb(90, 90, 90, 90));
        bar.addView(left, new LinearLayout.LayoutParams(0, 6, Math.max(1, 100 - pr)));

        LinearLayout.LayoutParams blp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, 6
        );
        blp.setMargins(0, 6, 0, 0);
        bar.setLayoutParams(blp);

        return bar;
    }

    private LinearLayout canalItem(ChannelItem ch, boolean foco) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setGravity(Gravity.CENTER_VERTICAL);
        row.setPadding(12, 8, 12, 8);

        GradientDrawable bg = new GradientDrawable();
        bg.setCornerRadius(12);

        if (foco) {
            bg.setColor(Color.argb(125, 170, 220, 0));
            bg.setStroke(2, Color.argb(240, 255, 235, 0));
        } else {
            bg.setColor(Color.argb(22, 0, 25, 45));
            bg.setStroke(1, Color.argb(38, 0, 210, 255));
        }

        row.setBackground(bg);

        ImageView logo = new ImageView(this);
        logo.setScaleType(ImageView.ScaleType.FIT_CENTER);
        LinearLayout.LayoutParams lpLogo = new LinearLayout.LayoutParams(130, 86);
        lpLogo.setMargins(0, 0, 16, 0);
        row.addView(logo, lpLogo);
        cargarLogoEn(logo, ch.logo);

        LinearLayout texts = new LinearLayout(this);
        texts.setOrientation(LinearLayout.VERTICAL);

        TextView name = tv(ch.name == null ? "Canal" : ch.name, 14, Color.WHITE);
        name.setSingleLine(true);
        name.setEllipsize(TextUtils.TruncateAt.END);

        String prog = (ch.now == null || ch.now.trim().isEmpty()) ? "Programación en Actualización" : ch.now;
        TextView now = tv(prog, 12, Color.LTGRAY);
        now.setSingleLine(true);
        now.setEllipsize(TextUtils.TruncateAt.END);

        texts.addView(name, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
        ));

        texts.addView(now, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
        ));

        texts.addView(barraProgresoCanal(ch.st, ch.en));

        row.addView(texts, new LinearLayout.LayoutParams(
                0,
                LinearLayout.LayoutParams.WRAP_CONTENT,
                1
        ));

        LinearLayout.LayoutParams rowLp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                108
        );
        rowLp.setMargins(0, 4, 0, 4);
        row.setLayoutParams(rowLp);

        return row;
    }


    private void reconstruirMenus() {
        long now = System.currentTimeMillis();

        if ((now - lastMenuRebuildMs) < 80) {
            if (!menuRebuildPending) {
                menuRebuildPending = true;
                handler.postDelayed(() -> {
                    menuRebuildPending = false;
                    reconstruirMenusAhora();
                }, 80);
            }
            return;
        }

        reconstruirMenusAhora();
    }

    private void reconstruirMenusAhora() {
        lastMenuRebuildMs = System.currentTimeMillis();

        try {
            if (guideMenu != null) root.removeView(guideMenu);
            if (catMenu != null) root.removeView(catMenu);
        } catch(Exception ignored) {}

        crearMenusLaterales();
    }


    private void moverCategoriaFoco(int delta) {
        if (categoryItems == null || categoryItems.size() == 0) return;

        long now = System.currentTimeMillis();
        if ((now - lastCategoryMoveMs) < 90) return;
        lastCategoryMoveMs = now;

        focusedCategoryIndex += delta;

        if (focusedCategoryIndex < 0) {
            focusedCategoryIndex = categoryItems.size() - 1;
        }

        if (focusedCategoryIndex >= categoryItems.size()) {
            focusedCategoryIndex = 0;
        }

        CategoryItem ci = categoriaEnFoco();
        if (ci != null) {
            catName = ci.name;
            focusedChannelIndex = 0;
            lastFocusedEpgSid = "";
            pendingCategoryId = ci.id;

            reconstruirMenus();
            if (menuState == 2 && catMenu != null) catMenu.setVisibility(View.VISIBLE);

            handler.removeCallbacksAndMessages(null);
            handler.postDelayed(() -> {
                CategoryItem current = categoriaEnFoco();
                if (current != null && current.id.equals(pendingCategoryId)) {
                    cargarMenuAndroid(current.id);
                }
            }, 120);
        }
    }

    private void moverColumnaCategoria(int dir) {
        catActiveColumn += dir;
        if (catActiveColumn < 0) catActiveColumn = 0;
        if (catActiveColumn > 1) catActiveColumn = 1;

        reconstruirMenus();

        if (menuState == 2 && catMenu != null) catMenu.setVisibility(View.VISIBLE);
    }


    private ChannelItem canalEnFoco() {
        if (channelItems == null || channelItems.size() == 0) return null;
        if (focusedChannelIndex < 0) focusedChannelIndex = 0;
        if (focusedChannelIndex >= channelItems.size()) focusedChannelIndex = channelItems.size() - 1;
        return channelItems.get(focusedChannelIndex);
    }



    private void cargarEpgPorSid(String sid) {
        return;
    }


    private void precargarEpgCategoriaCompleta() {
        return;
    }


    private void precargarEpgCanalesVisibles() {
        return;
    }


    private void cargarEpgCanalEnfocado() {
        return;
    }


    private void moverCanalFoco(int delta) {

        long now = System.currentTimeMillis();

        if ((now - lastChannelMoveMs) < 40) {
            return;
        }

        lastChannelMoveMs = now;

        if (channelItems == null || channelItems.size() == 0) return;

        focusedChannelIndex += delta;

        if (focusedChannelIndex < 0) {
            focusedChannelIndex = channelItems.size() - 1;
        }

        if (focusedChannelIndex >= channelItems.size()) {
            focusedChannelIndex = 0;
        }

        reconstruirMenus();

        if (menuState == 1 && guideMenu != null) guideMenu.setVisibility(View.VISIBLE);
        if (menuState == 2 && catMenu != null) catMenu.setVisibility(View.VISIBLE);
    }


    private static class EpgItem {
        String title = "";
        String desc = "";
        long start = 0;
        long stop = 0;
    }

    private void cargarEpgUnaVez() {
        actualizarEpgDesdeCacheLocal();
    }


    private void iniciarEpgLocal() {
        epgTicker = new Runnable() {
            @Override
            public void run() {
                actualizarEpgDesdeCacheLocal();
                handler.postDelayed(this, 1000);
            }
        };
        handler.postDelayed(epgTicker, 30000);
    }

    private void actualizarEpgDesdeCacheLocal() {
        if (epgItems == null || epgItems.size() == 0) {
            actualizarEpgViews();
            return;
        }

        long now = System.currentTimeMillis() / 1000L;

        try {
            long finActual = Long.parseLong(endTime == null || endTime.trim().isEmpty() ? "0" : endTime.trim());
            if (finActual > 0 && now >= finActual && !epgReloading && (now - lastEpgReload) > 20) {
                epgReloading = true;
                lastEpgReload = now;
                cargarEpgUnaVez();
                epgReloading = false;
                return;
            }
        } catch(Exception ignored) {}
        EpgItem current = null;
        EpgItem next = null;

        for (int i = 0; i < epgItems.size(); i++) {
            EpgItem it = epgItems.get(i);
            if (now >= it.start && now < it.stop) {
                current = it;
                if (i + 1 < epgItems.size()) next = epgItems.get(i + 1);
                break;
            }
            if (it.start > now) {
                next = it;
                break;
            }
        }

        if (current != null) {
            nowTitle = current.title;
            startTime = String.valueOf(current.start);
            endTime = String.valueOf(current.stop);
        }

        if (next != null) {
            nextTitle = next.title;
            nextStartTime = String.valueOf(next.start);
            nextEndTime = String.valueOf(next.stop);
        }

        actualizarEpgViews();
    }

    private void actualizarEpgViews() {
        try {
            if (nowView != null) nowView.setText(labelColor("Ahora: ", nowTitle, amarillo));
            if (nextView != null) nextView.setText(labelColor("Sigue: ", nextTitle, azul));
            if (nowTimeView != null) nowTimeView.setText(formatoHorario(startTime, endTime));
            if (nextTimeView != null) nextTimeView.setText(formatoHorario(nextStartTime, nextEndTime));
            if (remainView != null) remainView.setText(minutosRestantes(endTime));

            if (progressDone != null && progressLeft != null) {
                int progress = calcularProgreso(startTime, endTime);
                LinearLayout.LayoutParams lpDone = new LinearLayout.LayoutParams(0, 7, progress);
                LinearLayout.LayoutParams lpLeft = new LinearLayout.LayoutParams(0, 7, Math.max(1, 100 - progress));
                progressDone.setLayoutParams(lpDone);
                progressLeft.setLayoutParams(lpLeft);
            }
        } catch (Exception ignored) {}
    }

    private String formatoHorario(String ini, String fin) {
        String a = horaBonita(ini);
        String b = horaBonita(fin);
        if (a.isEmpty() || b.isEmpty()) return "";
        return a + " – " + b;
    }

    private String horaBonita(String raw) {
        try {
            long v = Long.parseLong(raw.trim());
            if (v > 9999999999L) v = v / 1000;
            return new SimpleDateFormat("hh:mm a", new Locale("es", "CL"))
                    .format(new Date(v * 1000))
                    .replace("a. m.", "a. m.")
                    .replace("p. m.", "p. m.");
        } catch (Exception e) {
            return "";
        }
    }

    private int calcularProgreso(String ini, String fin) {
        try {
            long s = Long.parseLong(ini.trim());
            long e = Long.parseLong(fin.trim());
            long n = System.currentTimeMillis() / 1000;

            if (s <= 0 || e <= 0 || e <= s) return 0;
            if (n <= s) return 0;
            if (n >= e) return 100;

            return (int)Math.max(0, Math.min(100, ((n - s) * 100) / (e - s)));
        } catch(Exception e) {
            return 0;
        }
    }

    private String minutosRestantes(String fin) {
        try {
            long e = Long.parseLong(fin.trim());
            long n = System.currentTimeMillis() / 1000;

            if (e <= 0 || e <= n) return "";

            long m = (e - n) / 60;
            if (m <= 0) return "";

            return m + " min";
        } catch(Exception e) {
            return "";
        }
    }

    private void cargarLogo(String logoUrl) {
        if (logoUrl == null || !logoUrl.startsWith("http")) return;

        new Thread(() -> {
            try {
                InputStream in = new URL(logoUrl).openStream();
                Bitmap bmp = BitmapFactory.decodeStream(in);
                runOnUiThread(() -> logoView.setImageBitmap(bmp));
            } catch (Exception ignored) {}
        }).start();
    }

    private void playUrl(String playUrl) {
        DefaultHttpDataSource.Factory httpFactory = new DefaultHttpDataSource.Factory()
                .setAllowCrossProtocolRedirects(true)
                .setConnectTimeoutMs(12000)
                .setReadTimeoutMs(12000)
                .setUserAgent("VLC/3.0.20 LibVLC/3.0.20");

        MediaItem item = new MediaItem.Builder().setUri(Uri.parse(playUrl)).build();
        String lower = playUrl.toLowerCase();

        if (lower.contains(".m3u8")) {
            /*
             * PZ_HLS_IPTV_FLAGS:
             * Para streams IPTV H264 que no entregan SPS/PPS limpios al inicio.
             * Reduce espera del decoder al permitir non-IDR keyframes y detectar access units.
             */
            int tsFlags =
                    DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES |
                    DefaultTsPayloadReaderFactory.FLAG_DETECT_ACCESS_UNITS;

            DefaultHlsExtractorFactory hlsExtractorFactory =
                    new DefaultHlsExtractorFactory(tsFlags, true);

            player.setMediaSource(new HlsMediaSource.Factory(httpFactory)
                    .setExtractorFactory(hlsExtractorFactory)
                    .setAllowChunklessPreparation(true)
                    .createMediaSource(item));
        } else {
            player.setMediaSource(new ProgressiveMediaSource.Factory(httpFactory).createMediaSource(item));
        }

        player.prepare();
        player.play();
    }


    private boolean cambiarCanalFullscreenDpad(int key) {
        if (key != KeyEvent.KEYCODE_DPAD_UP && key != KeyEvent.KEYCODE_DPAD_DOWN) return false;

        boolean guiaVisible = guideMenu != null && guideMenu.getVisibility() == View.VISIBLE;
        boolean catVisible = catMenu != null && catMenu.getVisibility() == View.VISIBLE;

        if (guiaVisible || catVisible || menuState != 0) return false;

        ArrayList<ChannelItem> lista = null;

        if (playingCategoryChannels != null && playingCategoryChannels.size() > 0) {
            lista = playingCategoryChannels;
        } else if (playingChannelItems != null && playingChannelItems.size() > 0) {
            lista = playingChannelItems;
        } else if (channelItems != null && channelItems.size() > 0) {
            playingCategoryChannels = copiarCanales(channelItems);
            lista = playingCategoryChannels;
        }

        if (lista == null || lista.size() == 0) return true;

        int actual = playingChannelIndex;
        if (actual < 0 || actual >= lista.size()) actual = focusedChannelIndex;
        if (actual < 0 || actual >= lista.size()) actual = 0;

        // ARRIBA = canal siguiente hacia abajo de la lista
        // ABAJO  = canal anterior hacia arriba de la lista
        int delta = (key == KeyEvent.KEYCODE_DPAD_UP) ? 1 : -1;

        int total = lista.size();
        int nextIndex = (actual + delta + total) % total;

        channelItems = copiarCanales(lista);
        focusedChannelIndex = nextIndex;
        playingChannelIndex = nextIndex;

        reproducirCanalEnFoco();
        mostrarOSD();
        return true;
    }

    private void mostrarOSD() {
        osdBox.setVisibility(View.VISIBLE);
        actualizarEpgDesdeCacheLocal();

        if (osdHideTicker != null) {
            handler.removeCallbacks(osdHideTicker);
        }

        osdHideTicker = () -> osdBox.setVisibility(View.GONE);
        handler.postDelayed(osdHideTicker, 10000);
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            int key = event.getKeyCode();

            if (cambiarCanalFullscreenDpad(key)) {
                return true;
            }

            if (key == KeyEvent.KEYCODE_DPAD_LEFT) {
                if (menuState == 0) {
                    mostrarGuiaMenu();
                } else if (menuState == 1) {
                    mostrarCategoriasMenu();
                } else if (menuState == 2) {
                    if (catActiveColumn == 1) {
                        moverColumnaCategoria(-1);
                    } else {
                        cerrarMenus();
                    }
                }
                return true;
            }

            if (key == KeyEvent.KEYCODE_DPAD_RIGHT) {
                if (menuState == 2) {
                    cargarCategoriaEnFoco();
                    menuState = 1;
                    catActiveColumn = 1;
                    blockEnterUntilMs = System.currentTimeMillis() + 450;
                    reconstruirMenus();
                    if (catMenu != null) catMenu.setVisibility(View.GONE);
                    if (guideMenu != null) guideMenu.setVisibility(View.VISIBLE);
                    if (osdBox != null) osdBox.setVisibility(View.GONE);
                    return true;
                }
            }

            if (key == KeyEvent.KEYCODE_DPAD_UP) {
                if (menuState == 1) {
                    moverCanalFoco(-1);
                    return true;
                }
                if (menuState == 2) {
                    if (catActiveColumn == 0) moverCategoriaFoco(-1);
                    else moverCanalFoco(-1);
                    return true;
                }
            }

            if (key == KeyEvent.KEYCODE_DPAD_DOWN) {
                if (menuState == 1) {
                    moverCanalFoco(1);
                    return true;
                }
                if (menuState == 2) {
                    if (catActiveColumn == 0) moverCategoriaFoco(1);
                    else moverCanalFoco(1);
                    return true;
                }
            }

            if (key == KeyEvent.KEYCODE_BACK) {
                if (menuState != 0) {
                    cerrarMenus();
                    return true;
                }
            }

            if (key == KeyEvent.KEYCODE_DPAD_CENTER || key == KeyEvent.KEYCODE_ENTER) {
                if (System.currentTimeMillis() < blockEnterUntilMs) {
                    return true;
                }

                if (menuState == 2 && catActiveColumn == 0) {
                    cargarCategoriaEnFoco();
                    catActiveColumn = 1;
                    return true;
                }
                if (menuState != 0) {
                    reproducirCanalEnFoco();
                    return true;
                }
                mostrarOSD();
                return true;
            }
        }

        return super.dispatchKeyEvent(event);
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (cambiarCanalFullscreenDpad(keyCode)) {
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onDestroy() {
        try { if (epgTicker != null) handler.removeCallbacks(epgTicker); } catch (Exception ignored) {}
        try { if (osdHideTicker != null) handler.removeCallbacks(osdHideTicker); } catch (Exception ignored) {}
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
        if (player != null) {
            player.release();
            player = null;
        }
    }
}
