package com.nadi.player.nativeplayer.channels;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.nadi.player.R;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Encapsulates everything that happens inside the right-side channel
 * overlay: show/hide animation, search, category strip, channel list,
 * and UP/DOWN zapping.
 *
 * Data source is {@link ChannelStore} — populated by JS via the
 * Capacitor bridge before the player launches. If the store is empty
 * the overlay still inflates (so opening it doesn't crash) but renders
 * a single empty-state hint row.
 *
 * Performance: animations are pure View.animate() translationX + alpha,
 * which are hardware-composited and don't trigger layout passes.
 */
public final class ChannelOverlayController {

    public interface Listener {
        /** User picked a channel from the list (tap or D-pad center). */
        void onChannelSelected(Channel channel);
        /** Panel finished opening / closing — used to refocus the video view. */
        void onPanelVisibilityChanged(boolean visible);
    }

    private static final long ANIM_MS = 180L;

    private final View       panelRoot;
    private final RecyclerView listView;
    private final EditText   searchInput;
    private final LinearLayout categoryStrip;
    private final TextView   countLabel;
    private final TextView   emptyView;
    // Not final: the click-listener lambda below captures `adapter` and Java's
    // definite-assignment rule rejects reading a final field from inside the
    // very expression that assigns it. The lambda only fires after the
    // constructor completes, so non-final is safe and semantically identical.
    private ChannelListAdapter adapter;
    private final Listener   listener;
    private List<Channel> source;
    private List<ChannelStore.Category> categories;

    private String currentCategory = ChannelStore.Category.KEY_ALL;
    private String currentQuery = "";
    private boolean visible = false;
    private int currentChannelNumber = -1;
    /** Channel numbers tagged as "recently watched" in this session. */
    private final List<Integer> recentChannels = new ArrayList<>(8);

    public ChannelOverlayController(View panelRoot, Listener listener) {
        this.panelRoot = panelRoot;
        this.listener = listener;
        this.searchInput   = panelRoot.findViewById(R.id.panel_search);
        this.categoryStrip = panelRoot.findViewById(R.id.panel_categories);
        this.listView      = panelRoot.findViewById(R.id.panel_channel_list);
        this.countLabel    = panelRoot.findViewById(R.id.panel_count);
        this.emptyView     = panelRoot.findViewById(R.id.panel_empty);

        Context ctx = panelRoot.getContext();

        // Pull whatever JS pushed in. Calling this lazily on show() would
        // miss the rebuild after a config-change, so we snapshot now.
        ChannelStore store = ChannelStore.get();
        this.source = store.channels();
        this.categories = store.categories();
        this.currentChannelNumber = store.currentChannelNumber();

        this.adapter = new ChannelListAdapter((channel, position) -> {
            currentChannelNumber = channel.number;
            ChannelStore.get().setCurrentChannelNumber(channel.number);
            adapter.setSelectedChannelNumber(channel.number);
            pushRecent(channel.number);
            if (listener != null) listener.onChannelSelected(channel);
        });

        LinearLayoutManager lm = new LinearLayoutManager(ctx);
        lm.setInitialPrefetchItemCount(8); // keeps scroll silky on first fling
        listView.setLayoutManager(lm);
        listView.setHasFixedSize(true);
        listView.setItemAnimator(null);    // disable cross-fade flicker
        listView.setAdapter(adapter);

        wireSearch();
        buildCategoryStrip();
        applyFilter();

        // Panel starts off-screen to the right.
        panelRoot.post(() -> panelRoot.setTranslationX(panelRoot.getWidth()));
        panelRoot.setAlpha(0f);
        panelRoot.setVisibility(View.INVISIBLE);
    }

    /* ===== public API ===== */

    public boolean isVisible() {
        return visible;
    }

    public void show() {
        if (visible) return;
        visible = true;
        // Pull the freshest "now playing" number from the store — it may
        // have changed since this controller was constructed if a
        // lock-screen ⏭ / ⏮ or the home-screen widget zapped while the
        // panel was closed.
        currentChannelNumber = ChannelStore.get().currentChannelNumber();
        adapter.setSelectedChannelNumber(currentChannelNumber);
        panelRoot.setVisibility(View.VISIBLE);
        panelRoot.animate().cancel();
        panelRoot.animate()
                .translationX(0f)
                .alpha(1f)
                .setDuration(ANIM_MS)
                .setListener(new AnimatorListenerAdapter() {
                    @Override public void onAnimationEnd(Animator a) {
                        listView.requestFocus();
                        if (currentChannelNumber > 0) {
                            int idx = adapter.indexOfChannel(currentChannelNumber);
                            if (idx >= 0) listView.scrollToPosition(idx);
                        }
                        if (listener != null) listener.onPanelVisibilityChanged(true);
                    }
                })
                .start();
    }

    public void hide() {
        if (!visible) return;
        visible = false;
        panelRoot.animate().cancel();
        panelRoot.animate()
                .translationX(panelRoot.getWidth())
                .alpha(0f)
                .setDuration(ANIM_MS)
                .setListener(new AnimatorListenerAdapter() {
                    @Override public void onAnimationEnd(Animator a) {
                        panelRoot.setVisibility(View.INVISIBLE);
                        if (listener != null) listener.onPanelVisibilityChanged(false);
                    }
                })
                .start();
    }

    public void toggle() {
        if (visible) hide(); else show();
    }

    /** Mark the currently playing channel — drives the gold "now playing"
     *  highlight in the list and is the anchor for UP/DOWN zapping. */
    public void setCurrentChannelNumber(int number) {
        currentChannelNumber = number;
        ChannelStore.get().setCurrentChannelNumber(number);
        adapter.setSelectedChannelNumber(number);
        pushRecent(number);
    }

    /** UP key → previous channel in the *currently filtered* list. */
    public Channel zapPrevious() {
        return zap(-1);
    }

    /** DOWN key → next channel in the *currently filtered* list. */
    public Channel zapNext() {
        return zap(+1);
    }

    /* ===== internals ===== */

    private Channel zap(int delta) {
        int count = adapter.getItemCount();
        if (count == 0) return null;
        int cur = adapter.indexOfChannel(currentChannelNumber);
        int next;
        if (cur < 0) {
            next = (delta > 0) ? 0 : count - 1;
        } else {
            next = (cur + delta + count) % count;
        }
        Channel ch = adapter.get(next);
        if (ch != null) {
            currentChannelNumber = ch.number;
            ChannelStore.get().setCurrentChannelNumber(ch.number);
            adapter.setSelectedChannelNumber(ch.number);
            pushRecent(ch.number);
            listView.scrollToPosition(next);
            if (listener != null) listener.onChannelSelected(ch);
        }
        return ch;
    }

    private void wireSearch() {
        searchInput.addTextChangedListener(new TextWatcher() {
            @Override public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
            @Override public void onTextChanged(CharSequence s, int a, int b, int c) {
                currentQuery = s == null ? "" : s.toString().trim();
                applyFilter();
            }
            @Override public void afterTextChanged(Editable s) {}
        });
    }

    private void buildCategoryStrip() {
        categoryStrip.removeAllViews();
        LayoutInflater inflater = LayoutInflater.from(categoryStrip.getContext());
        for (ChannelStore.Category cat : categories) {
            final TextView chip = (TextView) inflater.inflate(
                    R.layout.item_category_chip, categoryStrip, false);
            chip.setText(cat.label);
            chip.setSelected(cat.key.equals(currentCategory));
            chip.setOnClickListener(v -> {
                if (cat.key.equals(currentCategory)) return;
                currentCategory = cat.key;
                refreshCategoryStates();
                applyFilter();
            });
            ViewGroup.MarginLayoutParams lp =
                    (ViewGroup.MarginLayoutParams) chip.getLayoutParams();
            categoryStrip.addView(chip, lp);
        }
    }

    private void refreshCategoryStates() {
        for (int i = 0; i < categoryStrip.getChildCount() && i < categories.size(); i++) {
            View v = categoryStrip.getChildAt(i);
            v.setSelected(categories.get(i).key.equals(currentCategory));
        }
    }

    private void applyFilter() {
        List<Channel> out = new ArrayList<>(source.size());
        String q = currentQuery.toLowerCase(Locale.getDefault());
        boolean isFav    = ChannelStore.Category.KEY_FAVORITES.equals(currentCategory);
        boolean isRecent = ChannelStore.Category.KEY_RECENT.equals(currentCategory);
        boolean isAll    = ChannelStore.Category.KEY_ALL.equals(currentCategory);

        if (isRecent) {
            for (Integer num : recentChannels) {
                for (Channel c : source) {
                    if (c.number == num && matchesQuery(c, q)) {
                        out.add(c);
                        break;
                    }
                }
            }
        } else {
            for (Channel c : source) {
                boolean catOk = isAll
                        || (isFav && c.favorite)
                        || c.category.equals(currentCategory);
                if (!catOk) continue;
                if (!matchesQuery(c, q)) continue;
                out.add(c);
            }
        }
        adapter.submit(out);
        adapter.setSelectedChannelNumber(currentChannelNumber);
        countLabel.setText(String.format(Locale.US, "%d", out.size()));

        // Empty-state hint — distinguishes "no playlist loaded" from
        // "search returned nothing" so the user knows what's going on.
        if (emptyView != null) {
            if (out.isEmpty()) {
                emptyView.setText(source.isEmpty()
                        ? "No channels available."
                        : "No matches.");
                emptyView.setVisibility(View.VISIBLE);
                listView.setVisibility(View.GONE);
            } else {
                emptyView.setVisibility(View.GONE);
                listView.setVisibility(View.VISIBLE);
            }
        }
    }

    private boolean matchesQuery(Channel c, String q) {
        if (q.isEmpty()) return true;
        return c.name.toLowerCase(Locale.getDefault()).contains(q);
    }

    private void pushRecent(int number) {
        if (number <= 0) return;
        recentChannels.remove((Integer) number);
        recentChannels.add(0, number);
        if (recentChannels.size() > 10) {
            recentChannels.remove(recentChannels.size() - 1);
        }
    }
}
