import re
import requests
import sqlite3
import os
import json
import time
from urllib.parse import urlparse, parse_qs, quote_plus
from threading import Thread

import xbmc
import xbmcgui
import xbmcaddon
from xbmcvfs import translatePath

from ..plugin import Plugin
from ..util.dialogs import link_dialog
from resources.lib.plugin import run_hook

# Addon paths
addon = xbmcaddon.Addon()
USER_DATA_DIR = translatePath(addon.getAddonInfo("profile"))
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "file_iptv_config.json")
DB_PATH = os.path.join(USER_DATA_DIR, "file_iptv.db")
RECENT_PATH = os.path.join(USER_DATA_DIR, "file_iptv_recent.json")

if not os.path.exists(USER_DATA_DIR):
    os.makedirs(USER_DATA_DIR)

# Basic HTTP headers to improve compatibility with some panels
HTTP_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) "
        "Gecko/20100101 Firefox/117.0"
    ),
    "Accept": "*/*",
    "Referer": "",
}


M3U_USERNAME = "__M3U__"

# Config
DEFAULT_CONFIG = {
    "servers_txt": "http://magnetic.website/MAD_TITAN_SPORTS/Keep_m3u_json/zone1.txt",
    "exclude_groups": ["xxx", "adult", "18+"],
    "total_result_limit": 1000,
    "per_address_limit": 1000,
    "recheck_hours": 12,
    "min_connections": 2,
    "max_servers_per_update": 50,
    "stream_check_enabled": True,
    "stream_check_candidates": 5,
    "test_channel_keywords": [
        "espn", "sky sports", "tnt", "nba", "nfl", "tsn", "fox sports", "nbc sports"
    ],
    "m3u_sources": [],
    "exclude_names": [
        "BR:", "ARG:", "MEX:"
    ],
    "proxy_mode": "auto",
    "auto_update_enabled": False,
    "last_auto_update_ts": 0,
    "debug_logging": False,
}

# Load config with proper merging
try:
    with open(CONFIG_PATH, "r", encoding="utf-8") as f:
        loaded = json.load(f)
    
    config = DEFAULT_CONFIG.copy()
    
    # Merge loaded config (preserve user values, especially lists)
    for k, v in loaded.items():
        if k in config:
            if isinstance(config[k], list) and isinstance(v, list):
                # For lists, we usually want to fully replace with user's version
                config[k] = v
            else:
                config[k] = v
        else:
            # Keep new defaults that aren't in old JSON
            config[k] = v

except Exception as e:
    xbmc.log(f"[file_iptv] Failed to load config, using defaults: {e}", xbmc.LOGWARNING)
    config = DEFAULT_CONFIG.copy()

# Ensure config file exists and is up-to-date
try:
    with open(CONFIG_PATH, "w", encoding="utf-8") as f:
        json.dump(config, f, indent=4, ensure_ascii=False)
except Exception as e:
    xbmc.log(f"[file_iptv] Failed to write config: {e}", xbmc.LOGERROR)


def save_config():
    """Save current config to disk."""
    try:
        with open(CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(config, f, indent=4)
    except Exception as e:
        xbmc.log(f"[file_iptv] Failed to save config: {e}", xbmc.LOGERROR)


def should_auto_update_now() -> bool:
    """Determine if enough time has passed to run an automatic update."""
    enabled = bool(config.get("auto_update_enabled", True))
    if not enabled:
        return False

    try:
        recheck_hours = int(config.get("recheck_hours", 12))
    except Exception:
        recheck_hours = 12
    if recheck_hours <= 0:
        # 0 or negative means "no scheduled auto-update"
        return False

    interval = recheck_hours * 3600
    now = int(time.time())
    last_ts = int(config.get("last_auto_update_ts", 0) or 0)
    return (now - last_ts) >= interval

def init_db():
    with sqlite3.connect(DB_PATH) as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS servers (
                address TEXT,
                username TEXT,
                password TEXT,
                last_checked INTEGER DEFAULT 0,
                is_valid INTEGER DEFAULT 0,
                PRIMARY KEY (address, username, password)
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS channels (
                address TEXT,
                username TEXT,
                password TEXT,
                stream_id TEXT,
                name TEXT,
                search TEXT,
                stream_url TEXT,
                PRIMARY KEY (address, username, password, stream_id),
                FOREIGN KEY (address, username, password)
                    REFERENCES servers(address, username, password)
            )
        """)
        # Ensure newer columns exist
        try:
            conn.execute("ALTER TABLE servers ADD COLUMN max_connections INTEGER DEFAULT 0")
        except Exception:
            pass
        try:
            conn.execute("ALTER TABLE channels ADD COLUMN stream_url TEXT")
        except Exception:
            pass
        # Whether this server's channels should appear in search results.
        # Keep it separate from is_valid so a server can stay in the
        # database and keep updating but be hidden from the UI if desired.
        try:
            conn.execute("ALTER TABLE servers ADD COLUMN search_enabled INTEGER DEFAULT 1")
        except Exception:
            pass
        conn.commit()


def load_recent_searches(max_items: int = 20):
    """Load recent search history from JSON file."""
    if not os.path.exists(RECENT_PATH):
        return []
    try:
        with open(RECENT_PATH, "r", encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, list):
            return data[:max_items]
    except Exception as e:
        xbmc.log(f"[file_iptv] Failed to load recent searches: {e}", xbmc.LOGERROR)
    return []


def save_recent_search(term, country):
    """Append a search to history, de-duplicating on (term,country)."""
    entry = {
        "term": term,
        "country": country or "",
        "time": int(time.time()),
    }
    history = load_recent_searches(max_items=50)
    # Remove existing identical entry
    history = [h for h in history if not (h.get("term") == entry["term"] and h.get("country", "") == entry["country"])]
    history.insert(0, entry)
    # Trim
    if len(history) > 50:
        history = history[:50]
    try:
        with open(RECENT_PATH, "w", encoding="utf-8") as f:
            json.dump(history, f, indent=2)
    except Exception as e:
        xbmc.log(f"[file_iptv] Failed to save recent searches: {e}", xbmc.LOGERROR)

def clean_string(s: str) -> str:
    s = s.strip()
    # Remove leading country/region codes like "US - ", "UK: ", but
    # do NOT strip normal words like "FOX " or "TNT ". Require a separator.
    s = re.sub(r'^[A-Z]{2,3}[-:]\s+', '', s, flags=re.IGNORECASE)
    s = re.sub(r'\|\s?[A-Z]{2,3}\s?(8K|4K|UHD|FHD|HD|SD|HDR)$', '', s, flags=re.IGNORECASE)
    s = re.sub(r'\|\s?(8K|4K|UHD|FHD|HD|SD|HDR)\s?[A-Z]{2,3}$', '', s, flags=re.IGNORECASE)
    s = re.sub(r'\|\s?[A-Z]{2,3}$', '', s, flags=re.IGNORECASE)
    s = re.sub(r'\b(8k|4k|uhd|fhd|hd|sd|hdr)\b', '', s, flags=re.IGNORECASE)
    s = s.lower()
    s = s.replace('bally', 'fanduel')  # Do NOT strip 'network'
    s = re.sub(r".*[|\-:\]]\s+(.*)", r"\1", s)
    s = re.sub(r'^\(.*?\)', '', s)
    s = re.sub(r'\(.*?\)$', '', s)
    s = re.sub(r'\[.*?\]$', '', s)
    s = re.sub(r"[^a-z0-9]", "", s)
    return s.strip()

def parse_server_url(line: str):
    """Parse one m3u/m3u_plus URL into (address, username, password)."""
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    try:
        parsed = urlparse(line)
        if parsed.scheme not in ("http", "https"):
            return None

        # Strip userinfo (e.g. "tvappapk@" in tvappapk@tv.zapping.life:8080)
        netloc = parsed.netloc.split("@", 1)[-1]
        address = f"{parsed.scheme}://{netloc}"

        qs = parse_qs(parsed.query)
        username = qs.get("username", [""])[0]
        password = qs.get("password", [""])[0]
        if not (address and username and password):
            return None
        return (address, username, password)
    except Exception as e:
        xbmc.log(f"[file_iptv] Failed to parse server URL '{line}': {e}", xbmc.LOGERROR)
        return None


def load_m3u_sources():
    """Return a list of generic M3U playlist sources from config."""
    sources = config.get("m3u_sources", [])
    if isinstance(sources, str):
        out = [s.strip() for s in sources.split(",") if s.strip()]
    elif isinstance(sources, list):
        out = [str(s).strip() for s in sources if str(s).strip()]
    else:
        out = []
    return out

def load_servers_from_txt():
    """Load up to the needed number of unique (address, username, password) tuples from configured TXT."""
    path = config.get("servers_txt", "").strip()
    if not path:
        return []

    try:
        val = config.get("max_servers_per_update", 50)
        max_needed = int(val) if val is not None else 50
        if path.startswith(("http://", "https://")):
            resp = requests.get(path, timeout=10, headers=HTTP_HEADERS)
            resp.raise_for_status()
            lines = resp.text.splitlines()
        else:
            local_path = translatePath(path)
            if not os.path.exists(local_path):
                xbmc.log(f"[file_iptv] TXT file not found: {local_path}", xbmc.LOGWARNING)
                return []
            with open(local_path, "r", encoding="utf-8") as f:
                lines = f.readlines()

        servers = []
        seen = set()
        m3u_found = set()
        for line in lines:
            if len(servers) >= max_needed:
                break
            cred = parse_server_url(line)
            if cred and cred not in seen:
                seen.add(cred)
                servers.append(cred)
            else:
                candidate = line.strip()
                if (
                    candidate
                    and not candidate.startswith("#")
                    and candidate.startswith(("http://", "https://"))
                    and ".m3u" in candidate.lower()
                ):
                    m3u_found.add(candidate)

        # If any generic M3U/M3U8 URLs are present in the TXT, merge them
        # into the m3u_sources config so they will be indexed by
        # update_m3u_playlists().
        if m3u_found:
            existing = load_m3u_sources()
            combined = []
            seen_src = set()
            for src in existing + list(m3u_found):
                if src not in seen_src:
                    seen_src.add(src)
                    combined.append(src)
            config["m3u_sources"] = combined
            save_config()

        xbmc.log(
            f"[file_iptv] Loaded {len(servers)} xtream servers from TXT; {len(m3u_found)} M3U sources detected",
            xbmc.LOGINFO,
        )
        return servers
    except Exception as e:
        xbmc.log(f"[file_iptv] Error loading TXT: {e}", xbmc.LOGERROR)
        return []


def update_m3u_playlists():
    """Fetch and index channels from configured M3U playlists."""
    sources = load_m3u_sources()
    if not sources:
        return

    now = int(time.time())

    for src in sources:
        try:
            # Fetch playlist content
            if src.startswith(("http://", "https://")):
                resp = requests.get(src, timeout=15, headers=HTTP_HEADERS)
                resp.raise_for_status()
                text = resp.text
            else:
                local_path = translatePath(src)
                if not os.path.exists(local_path):
                    xbmc.log(f"[file_iptv] M3U file not found: {local_path}", xbmc.LOGWARNING)
                    continue
                with open(local_path, "r", encoding="utf-8", errors="ignore") as f:
                    text = f.read()

            channels = []
            current_name = None
            for raw_line in text.splitlines():
                line = raw_line.strip()
                if not line:
                    continue
                if line.startswith("#EXTINF"):
                    # Standard M3U: name is after the last comma
                    if "," in line:
                        name = line.split(",", 1)[1].strip()
                    else:
                        name = line
                    current_name = name
                elif line.startswith("#"):
                    continue
                else:
                    url = line
                    name = current_name or url
                    channels.append((name, url))
                    current_name = None

            with sqlite3.connect(DB_PATH) as conn:
                cur = conn.cursor()
                # Represent each playlist as a synthetic server keyed by the
                # playlist URL/path and M3U_USERNAME.
                cur.execute(
                    """
                    INSERT OR REPLACE INTO servers
                    (address, username, password, last_checked, is_valid, max_connections)
                    VALUES (?, ?, ?, ?, 1, 0)
                    """,
                    (src, M3U_USERNAME, "", now),
                )

                # Clear any existing channels for this playlist
                cur.execute(
                    """
                    DELETE FROM channels
                    WHERE address = ? AND username = ? AND password = ?
                    """,
                    (src, M3U_USERNAME, ""),
                )

                for idx, (name, url) in enumerate(channels):
                    name = name.strip()
                    if not name or not url:
                        continue

                    # Apply same exclusions as Xtream servers
                    name_l = name.lower()
                    exclude_names = [n.lower() for n in config.get("exclude_names", [])]
                    exclude_groups = [g.lower() for g in config.get("exclude_groups", [])]

                    if any(ex in name_l for ex in exclude_names):
                        continue

                    # For M3U we don't have category_name, so we do our best with name only
                    if any(ex in name_l for ex in exclude_groups):
                        continue

                    # Extra M3U-specific filters
                    if any(word in name_l for word in ["movie", "film", "vod", "serie", "season", "episode", "24/7"]):
                        continue

                    search = clean_string(name)
                    stream_id = f"m3u_{idx}"

                    cur.execute(
                        """
                        INSERT OR REPLACE INTO channels
                        (address, username, password, stream_id, name, search, stream_url)
                        VALUES (?, ?, ?, ?, ?, ?, ?)
                        """,
                        (src, M3U_USERNAME, "", stream_id, name, search, url),
                    )

                conn.commit()

            xbmc.log(
                f"[file_iptv] Indexed {len(channels)} channels from M3U source: {src}",
                xbmc.LOGINFO,
            )
        except Exception as e:
            xbmc.log(f"[file_iptv] Failed to load M3U source {src}: {e}", xbmc.LOGERROR)

def validate_server(server, min_conn_override=None, stream_check_override=None):
    """Validate an IPTV server and get its channels using Xtream API."""
    address, username, password = server
    try:
        params = {"username": username, "password": password}

        try:
            user_data = requests.get(
                f"{address}/player_api.php",
                timeout=12,
                params=params,
                headers=HTTP_HEADERS,
            ).json()
        except requests.exceptions.RequestException as e:
            xbmc.log(f"[file_iptv] Server failed {address}: {e}", xbmc.LOGINFO)
            return False

        max_conn_raw = user_data.get("user_info", {}).get("max_connections", 0)
        try:
            max_conn = int(max_conn_raw or 0)
        except Exception:
            max_conn = 0

        # Require at least the configured minimum number of connections
        try:
            if min_conn_override is None:
                min_conn_required = int(config.get("min_connections", 2) or 0)
            else:
                min_conn_required = int(min_conn_override)
        except Exception:
            min_conn_required = 0
        if min_conn_required < 0:
            min_conn_required = 0

        if max_conn < min_conn_required:
            xbmc.log(
                f"[file_iptv] Server failed: Max connections {max_conn} < required {min_conn_required}",
                xbmc.LOGINFO,
            )
            return False

        status = user_data.get("user_info", {}).get("status", "")
        if status != "Active":
            xbmc.log(f"[file_iptv] Server failed: Status = {status}", xbmc.LOGINFO)
            return False

        try:
            panel_data = requests.get(
                f"{address}/panel_api.php",
                timeout=25,
                params=params,
                headers=HTTP_HEADERS,
            ).json()
        except requests.exceptions.RequestException as e:
            xbmc.log(f"[file_iptv] Server failed {address}: {e}", xbmc.LOGINFO)
            return False

        if "available_channels" not in panel_data:
            xbmc.log(f"[file_iptv] Server failed: No available_channels", xbmc.LOGINFO)
            return False

        # Optional stream reachability check, controlled by config.
        if stream_check_override is None:
            do_stream_check = bool(config.get("stream_check_enabled", True))
        else:
            do_stream_check = bool(stream_check_override)

        if do_stream_check:
            channels = list(panel_data.get("available_channels", {}).values())
            if not channels:
                xbmc.log(f"[file_iptv] Server failed: available_channels is empty", xbmc.LOGINFO)
                return False

            # Prefer sports-branded test channels if present; otherwise fall
            # back to the first few channels.
            default_keywords = [
                "espn",
                "sky sports",
                "tnt",
                "nba",
                "nfl",
                "tsn",
                "fox sports",
                "nbc sports",
            ]
            cfg_keywords = config.get("test_channel_keywords", default_keywords)
            if isinstance(cfg_keywords, str):
                # Allow comma-separated string in config; normalize to list
                test_keywords = [k.strip().lower() for k in cfg_keywords.split(",") if k.strip()]
            else:
                test_keywords = [str(k).strip().lower() for k in cfg_keywords if str(k).strip()]
            if not test_keywords:
                test_keywords = default_keywords

            def is_preferred_channel(ch):
                name = (ch.get("name") or "").lower()
                return any(k in name for k in test_keywords)

            preferred = [ch for ch in channels if is_preferred_channel(ch)]

            try:
                max_candidates = int(config.get("stream_check_candidates", 5) or 5)
            except Exception:
                max_candidates = 5
            if max_candidates <= 0:
                max_candidates = 1

            candidates = preferred[:max_candidates] if preferred else channels[:max_candidates]

            stream_ok = False
            for ch in candidates:
                stream_id = ch.get("stream_id")
                if not stream_id:
                    continue
                # Some panels use HLS (.m3u8), others TS (.ts). Try both
                # extensions before deciding the stream is unreachable.
                for ext in ("m3u8", "ts"):
                    test_url = f"{address}/live/{username}/{password}/{stream_id}.{ext}"
                    try:
                        with requests.get(test_url, timeout=7, stream=True, headers=HTTP_HEADERS) as resp:
                            if resp.status_code != 200:
                                continue
                            try:
                                next(resp.iter_content(chunk_size=2048))
                            except StopIteration:
                                pass
                            stream_ok = True
                            break
                    except Exception as e:
                        xbmc.log(f"[file_iptv] Test stream failed {test_url}: {e}", xbmc.LOGDEBUG)
                if stream_ok:
                    break

            if not stream_ok:
                xbmc.log(
                    f"[file_iptv] Server failed: no test channel playable via HTTP",
                    xbmc.LOGINFO,
                )
                return False

        xbmc.log(f"[file_iptv] Server validated: {address} (max_conn={max_conn})", xbmc.LOGINFO)
        return panel_data, max_conn
    except (json.JSONDecodeError, ValueError) as e:
        xbmc.log(f"[file_iptv] Server failed {address}: Invalid JSON (server dead or offline)", xbmc.LOGINFO)
        return False
    except requests.exceptions.RequestException as e:
        xbmc.log(f"[file_iptv] Server validation error {address}: {e}", xbmc.LOGERROR)
        return None
    except Exception as e:
        xbmc.log(f"[file_iptv] Server validation error {address}: {e}", xbmc.LOGERROR)
        return None

def store_server_channels(server, panel_data, max_conn):
    address, username, password = server
    try:
        with sqlite3.connect(DB_PATH) as conn:
            # Preserve search_enabled if server already exists
            cur = conn.cursor()
            cur.execute("""
                SELECT search_enabled FROM servers WHERE address = ? AND username = ? AND password = ?
            """, (address, username, password))
            row = cur.fetchone()
            if row is not None:
                search_enabled = row[0]
            else:
                search_enabled = 1
            conn.execute("""
                INSERT OR REPLACE INTO servers (address, username, password, last_checked, is_valid, max_connections, search_enabled)
                VALUES (?, ?, ?, ?, 1, ?, ?)
            """, (address, username, password, int(time.time()), int(max_conn), search_enabled))

            exclude_groups = [g.lower() for g in config.get("exclude_groups", [])]
            exclude_names = [n.lower() for n in config.get("exclude_names", [])]
            xbmc.log(f"[file_iptv] Exclude names loaded: {exclude_names}", xbmc.LOGINFO)
            xbmc.log(f"[file_iptv] Exclude groups loaded: {exclude_groups}", xbmc.LOGINFO)
            xbmc.log(f"[file_iptv] Using {len(exclude_names)} exclude name patterns", xbmc.LOGDEBUG)
            for channel in panel_data.get("available_channels", {}).values():
                name = (channel.get("name") or "").strip()
                if not name:
                    continue

                # Skip if name matches any excluded name pattern
                name_l = name.lower()
                if any(ex in name_l for ex in exclude_names):
                    continue

                category = (channel.get("category_name") or "").strip().lower()
                if any(ex in category for ex in exclude_groups):
                    continue

                search = clean_string(name)
                conn.execute("""
                    INSERT OR REPLACE INTO channels
                    (address, username, password, stream_id, name, search)
                    VALUES (?, ?, ?, ?, ?, ?)
                """, (address, username, password, channel["stream_id"], name, search))

            conn.commit()

        xbmc.log(f"[file_iptv] Stored channels for: {address}", xbmc.LOGINFO)
        return True
    except Exception as e:
        xbmc.log(f"[file_iptv] Failed to store {address}: {e}", xbmc.LOGERROR)
        return False

def search_channels(term, limit=20, per_address=5, country_filter=None):
    cleaned = clean_string(term)
    if config.get("debug_logging", False):
        xbmc.log(f"[file_iptv] search term='{term}' cleaned='{cleaned}'", xbmc.LOGINFO)

    like_pattern = f"%{cleaned}%"

    with sqlite3.connect(DB_PATH) as conn:
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        cur.execute("""
            SELECT c.address, c.username, c.password, c.stream_id, c.name, c.search, c.stream_url, s.max_connections
            FROM channels c
            JOIN servers s ON c.address = s.address
                          AND c.username = s.username
                          AND c.password = s.password
            WHERE s.is_valid = 1
              AND COALESCE(s.search_enabled, 1) = 1
              AND c.search LIKE ?
            ORDER BY LENGTH(c.search) - LENGTH(?), c.name
        """, (like_pattern, cleaned))
        rows = cur.fetchall()

    if config.get("debug_logging", False):
        xbmc.log(f"[file_iptv] total DB matches before filter: {len(rows)}", xbmc.LOGINFO)

    # Country filter
    if country_filter:
        cf = country_filter.strip().lower()
        if cf:
            rows = [r for r in rows if cf in (r["name"] or "").lower()]

    # Special handling for USA Network
    if cleaned.replace(" ", "") == "usanetwork" or term.strip().lower().replace(" ", "") == "usanetwork":
        rows = [r for r in rows if "usanetwork" in clean_string(r["name"])]

    # ESPN base search cleanup
    raw_term = (term or "").strip().lower()
    if raw_term == "espn":
        special_words = ["plus", "vs", "ncaab", "play", "2"]
        if not any(w in raw_term for w in special_words):
            rows = [r for r in rows if not any(w in (r["name"] or "").lower() for w in special_words)]

    # Filter out exact digit extensions
    is_digit = cleaned[-1].isdigit() if cleaned else False
    filtered = [
        r for r in rows
        if is_digit or not re.match(re.escape(cleaned) + r"\d", r["search"], re.IGNORECASE)
    ]

    # Better deduplication
    results = []
    seen = set()
    counts = {}

    for r in filtered:
        addr = r["address"]
        name_clean = clean_string(r["name"])
        
        key = (name_clean, addr)
        if key in seen:
            continue
        seen.add(key)

        counts.setdefault(addr, 0)
        if counts[addr] >= per_address:
            continue
        counts[addr] += 1

        # FIXED: Use r["key"] instead of r.get()
        conn_num = r["max_connections"] if r["max_connections"] is not None else 0
        label_num = "-" if not conn_num else str(conn_num)

        if r["stream_url"]:
            base_stream = r["stream_url"]
        else:
            base_stream = f"{r['address']}/live/{r['username']}/{r['password']}/{r['stream_id']}.m3u8"

        proxy_mode = config.get("proxy_mode", "auto")
        scheme = "jetproxy" if proxy_mode == "jetproxy" else \
                 "direct" if proxy_mode == "direct" else \
                 "direct" if label_num == "-" else "jetproxy"

        proxied_stream = f"{scheme}://{base_stream}"
        encoded_stream = quote_plus(proxied_stream)
        route_path = f"sportjetextractors/play?urls={encoded_stream}"
        plugin_url = f"plugin://plugin.video.madtitansports/{route_path}"

        server_label = f"{addr} (M3U)" if r["username"] == M3U_USERNAME else f"{addr} ({r['username'][:8]}...)"

        results.append({
            "name": f"[COLORyellow]{label_num}[/COLOR] {r['name']} [{server_label}]",
            "address": plugin_url,
            "path": route_path,
        })

        if len(results) >= limit:
            break

    if config.get("debug_logging", False):
        xbmc.log(f"[file_iptv] Final results after dedup: {len(results)}", xbmc.LOGINFO)

    return results

class FileIPTV(Plugin):
    name = "file_iptv"
    priority = 100

    def __init__(self):
        init_db()
        # Optionally kick off a background auto-update when the addon is
        # opened, based on the configured recheck_hours interval.
        if should_auto_update_now():
            def _worker():
                try:
                    xbmcgui.Dialog().notification(
                        "File IPTV",
                        "Auto-updating IPTV servers in background...",
                        xbmcgui.NOTIFICATION_INFO,
                        5000,
                    )
                except Exception:
                    pass

                try:
                    # Run update silently (no progress dialog/summary) and
                    # then show a small notification with the results.
                    stats = self.update_servers(show_progress=False, show_summary=False)
                    config["last_auto_update_ts"] = int(time.time())
                    save_config()

                    try:
                        if isinstance(stats, dict):
                            checked = int(stats.get("checked", 0) or 0)
                            valid = int(stats.get("validated", 0) or 0)
                            invalid = int(stats.get("invalidated", 0) or 0)
                            msg = f"Auto-update done | Checked: {checked}, Valid: {valid}, Removed: {invalid}"
                        else:
                            msg = "Auto-update complete (no servers needed refresh)."
                        xbmcgui.Dialog().notification(
                            "File IPTV",
                            msg,
                            xbmcgui.NOTIFICATION_INFO,
                            8000,
                        )
                    except Exception:
                        pass
                except Exception as e:
                    xbmc.log(f"[file_iptv] Auto-update error: {e}", xbmc.LOGERROR)

            t = Thread(target=_worker)
            t.daemon = True
            t.start()

    def process_item(self, item):
        # Full-page search variant
        if "full_file_iptv" in item:
            query = item.get("full_file_iptv")
            country = (item.get("country") or "").strip()

            # If configured with "full_file_iptv": "*" and country "*",
            # prompt for both channel and country, then show a full-page list.
            if query == "*" and item.get("country") == "*":
                item["link"] = f"{self.name}/full_search_prompt_country"
            elif query == "*":
                # Channel-only prompt, full-page list
                item["link"] = f"{self.name}/full_search_prompt"
            else:
                # Fixed term (optionally with fixed country filter)
                if country and country != "*":
                    item["link"] = f"{self.name}/full_search/{country}:::{query}"
                else:
                    item["link"] = f"{self.name}/full_search/{query}"

            # This opens a sub-directory (full page of results)
            item["is_dir"] = True
            item["list_item"] = xbmcgui.ListItem(item.get("title", item.get("name", "")))
            return item

        if self.name in item:
            query = item.get(self.name)
            country = (item.get("country") or "").strip()

            # Special value to open the Recent searches dialog
            if query == "recent":
                item["link"] = f"{self.name}/recent"
                item["is_dir"] = False
                item["list_item"] = xbmcgui.ListItem(item.get("title", item.get("name", "")))
                return item

            # If configured with "file_iptv": "*", defer to a prompt route
            if query == "*" and item.get("country") == "*":
                # Country + term prompt
                item["link"] = f"{self.name}/search_prompt_country"
            elif query == "*":
                # Term-only prompt
                item["link"] = f"{self.name}/search_prompt"
            else:
                # Fixed term (optionally with fixed country filter)
                if country and country != "*":
                    item["link"] = f"{self.name}/search/{country}:::{query}"
                else:
                    item["link"] = f"{self.name}/search/{query}"

            item["is_dir"] = False
            item["list_item"] = xbmcgui.ListItem(item.get("title", item.get("name", "")))
            return item

    def routes(self, plugin):
        @plugin.route(f"/{self.name}/recent")
        def recent():
            """Show recent searches in a dialog and re-run selection."""
            history = load_recent_searches()
            if not history:
                xbmcgui.Dialog().ok("Recent IPTV", "No recent searches yet.")
                return

            labels = []
            for h in history:
                term = h.get("term", "")
                country = h.get("country", "") or "Any"
                ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(h.get("time", 0)))
                labels.append(f"[B]{term}[/B]  [COLORyellow]({country})[/COLOR]\n[COLORgrey]{ts}[/COLOR]")

            idx = link_dialog(labels, return_idx=True, hide_links=False)
            if idx is None:
                return

            entry = history[idx]
            country = entry.get("country", "")
            term = entry.get("term", "")
            if not term:
                return

            if country:
                combined = f"{country}:::{term}"
            else:
                combined = term

            # Call the search handler directly instead of redirecting
            # through the router to avoid nested busy dialogs.
            search(combined)

        @plugin.route(f"/{self.name}/recent_entry/<idx>")
        def recent_entry(idx):
            """Re-run a recent search (opens dialog like normal search)."""
            try:
                idx = int(idx)
            except ValueError:
                return

            history = load_recent_searches()
            if idx < 0 or idx >= len(history):
                return

            entry = history[idx]
            country = entry.get("country", "")
            term = entry.get("term", "")
            if not term:
                return

            if country:
                combined = f"{country}:::{term}"
            else:
                combined = term

            # Reuse the main search handler directly
            search(combined)

        @plugin.route(f"/{self.name}/full_search_prompt_country")
        def full_search_prompt_country():
            """Full-page: prompt for channel, then optional country, then show list."""
            dlg = xbmcgui.Dialog()
            term = dlg.input("IPTV Search", type=xbmcgui.INPUT_ALPHANUM)
            if not term:
                return

            country = dlg.input("Country filter (e.g. US, USA)", type=xbmcgui.INPUT_ALPHANUM)
            if not country:
                country = ""

            combined = f"{country}:::{term}"
            plugin.redirect(f"/{self.name}/full_search/{combined}")

        @plugin.route(f"/{self.name}/full_search_prompt")
        def full_search_prompt():
            """Full-page: prompt only for channel, then show list."""
            dlg = xbmcgui.Dialog()
            term = dlg.input("IPTV Search", type=xbmcgui.INPUT_ALPHANUM)
            if not term:
                return

            plugin.redirect(f"/{self.name}/full_search/{term}")

        @plugin.route(f"/{self.name}/full_search/<query>")
        def full_search(query):
            # Decode optional country filter if present
            country_filter = None
            term = query
            if ":::" in query:
                country_filter, term = query.split(":::", 1)

            with sqlite3.connect(DB_PATH) as conn:
                cur = conn.cursor()
                cur.execute("SELECT COUNT(*) FROM servers WHERE is_valid = 1")
                server_count = cur.fetchone()[0]

            if server_count == 0:
                # Auto-load from TXT once if empty
                self.update_servers()

            results = search_channels(
                term,
                limit=config.get("total_result_limit", 20),
                per_address=config.get("per_address_limit", 5),
                country_filter=country_filter,
            )

            if not results:
                xbmcgui.Dialog().ok("IPTV Search", f"No channels found matching '{term}'")
                return

            # Save to recent history
            save_recent_search(term, country_filter)

            jen_list = []

            proxy_mode = config.get("proxy_mode", "auto")
            debug_on = bool(config.get("debug_logging", False))
            status_label = f"[COLORgrey]Proxy: {proxy_mode} | Debug: {'On' if debug_on else 'Off'}[/COLOR]"
            status_li = xbmcgui.ListItem(status_label)
            status_li.setProperty("IsPlayable", "false")
            jen_list.append({
                "link": "",  # non-clickable header row
                "is_dir": False,
                "list_item": status_li,
            })

            for r in results:
                li = xbmcgui.ListItem(r["name"])
                li.setProperty("IsPlayable", "true")
                jen_list.append({
                    "link": r["path"],
                    "is_dir": False,
                    "list_item": li,
                })

            run_hook("display_list", jen_list)

        @plugin.route(f"/{self.name}/search_prompt_country")
        def search_prompt_country():
            """Prompt for channel, then optional country, then call main search."""
            dlg = xbmcgui.Dialog()
            # 1) Ask for channel/search term first
            term = dlg.input("IPTV Search", type=xbmcgui.INPUT_ALPHANUM)
            if not term:
                return
            # 2) Then ask for country filter (can be left blank to mean no filter)
            country = dlg.input("Country filter (e.g. US, USA)", type=xbmcgui.INPUT_ALPHANUM)
            if not country:
                country = ""
            # Encode both pieces into one path segment, then parse in search()
            combined = f"{country}:::{term}"
            plugin.redirect(f"/{self.name}/search/{combined}")

        @plugin.route(f"/{self.name}/search_prompt")
        def search_prompt():
            """Prompt user for a search term, then reuse the normal search route."""
            dlg = xbmcgui.Dialog()
            query = dlg.input("IPTV Search", type=xbmcgui.INPUT_ALPHANUM)
            if not query:
                return

            plugin.redirect(f"/{self.name}/search/{query}")

        @plugin.route(f"/{self.name}/search/<query>")
        def search(query):
            # Decode optional country filter if present
            country_filter = None
            term = query
            if ":::" in query:
                country_filter, term = query.split(":::", 1)

            with sqlite3.connect(DB_PATH) as conn:
                cur = conn.cursor()
                cur.execute("SELECT COUNT(*) FROM servers WHERE is_valid = 1")
                server_count = cur.fetchone()[0]

            if server_count == 0:
                # Auto-load from TXT once if empty
                self.update_servers()

            results = search_channels(
                term,
                limit=config.get("total_result_limit", 20),
                per_address=config.get("per_address_limit", 5),
                country_filter=country_filter,
            )

            if not results:
                xbmcgui.Dialog().ok("IPTV Search", f"No channels found matching '{term}'")
                return

            # Save to recent history
            save_recent_search(term, country_filter)

            idx = link_dialog([r["name"] for r in results], return_idx=True, hide_links=False)
            if idx is None:
                return

            stream_url = results[idx]["address"]
            liz = xbmcgui.ListItem(results[idx]["name"])
            liz.setProperty("IsPlayable", "true")
            xbmc.Player().play(stream_url, liz)

        @plugin.route(f"/{self.name}/update")
        def update():
            xbmc.log("[file_iptv] update() called from settings menu", xbmc.LOGINFO)
            init_db()
            
            # Update both Xtream servers AND M3U sources
            self.update_servers()
            update_m3u_playlists()   # <-- Add this line
            
            xbmc.log("[file_iptv] update_servers() + update_m3u_playlists() finished", xbmc.LOGINFO)

        @plugin.route(f"/{self.name}/reset")
        def reset():
            dialog = xbmcgui.Dialog()
            if not dialog.yesno(
                "IPTV Reset",
                "Delete the database and start fresh? This will remove all servers/channels.",
            ):
                return
            try:
                if os.path.exists(DB_PATH):
                    os.remove(DB_PATH)
                    xbmc.log(f"[file_iptv] Deleted database: {DB_PATH}", xbmc.LOGINFO)
                dialog.ok("IPTV Reset", "Database cleared! Run update to rebuild.")
            except Exception as e:
                xbmc.log(f"[file_iptv] Reset error: {e}", xbmc.LOGERROR)
                dialog.ok("IPTV Reset", f"Error: {e}")

        @plugin.route(f"/{self.name}/manage_servers")
        def manage_servers():
            with sqlite3.connect(DB_PATH) as conn:
                conn.row_factory = sqlite3.Row
                cur = conn.cursor()
                cur.execute("""
                    SELECT s.address,
                           s.username,
                           s.password,
                           s.last_checked,
                           COALESCE(s.search_enabled, 1) AS search_enabled
                    FROM servers s
                    JOIN channels c ON c.address = s.address
                                    AND c.username = s.username
                                    AND c.password = s.password
                    WHERE s.is_valid = 1
                    GROUP BY s.address, s.username, s.password, s.last_checked, search_enabled
                    ORDER BY s.last_checked DESC
                    LIMIT 200
                """)
                # Convert rows to dicts so we can update search_enabled
                # in-memory when the user toggles it.
                servers = [dict(r) for r in cur.fetchall()]

            if not servers:
                xbmcgui.Dialog().ok("Manage Servers", "No servers with channels in database.")
                return

            server_list = []
            for row in servers:
                ts = time.strftime(
                    "%Y-%m-%d %H:%M",
                    time.localtime(row["last_checked"]),
                ) if row["last_checked"] else "Never"
                visible = "[B][COLORgreen]ON[/COLOR][/B]" if row.get("search_enabled", 1) else "[COLORred]OFF[/COLOR]"
                server_list.append(f"[{visible}] {row['address']} ({row['username'][:8]}...) - {ts}")

            while True:
                idx = link_dialog(server_list, return_idx=True, hide_links=False)
                if idx is None:
                    break

                selected_server = servers[idx]
                addr = selected_server["address"]
                user = selected_server["username"]
                pw = selected_server["password"]

                # Let the user choose to test, update, delete, or toggle visibility.
                action = xbmcgui.Dialog().select(
                    "Server action",
                    [
                        "Test server",
                        "Update server now",
                        "Delete server",
                        "Toggle in search results",
                        "Back",
                    ],
                )
                if action in (-1, 4):
                    continue

                # Test the selected server without modifying the DB
                if action == 0:
                    result = validate_server((addr, user, pw))
                    if isinstance(result, tuple):
                        panel_data, max_conn = result
                        ch_count = len(panel_data.get("available_channels", {}))
                        xbmcgui.Dialog().ok(
                            "Test Server",
                            f"Server OK\nMax connections: {max_conn}\nChannels: {ch_count}",
                        )
                    elif result is False:
                        xbmcgui.Dialog().ok(
                            "Test Server",
                            "Server failed validation.\n(See log for details.)",
                        )
                    else:
                        xbmcgui.Dialog().ok(
                            "Test Server",
                            "Transient error while testing.\n(See log for details.)",
                        )
                    continue

                # Update the selected server and refresh its channels in the DB
                if action == 1:
                    result = validate_server((addr, user, pw))
                    # Valid server: store/refresh channels
                    if isinstance(result, tuple):
                        panel_data, max_conn = result
                        ch_count = len(panel_data.get("available_channels", {}))
                        stored_ok = store_server_channels((addr, user, pw), panel_data, max_conn)
                        if stored_ok:
                            xbmcgui.Dialog().ok(
                                "Update Server",
                                f"Server updated.\nMax connections: {max_conn}\nChannels: {ch_count}",
                            )
                        else:
                            xbmcgui.Dialog().ok(
                                "Update Server",
                                "Server validated but failed to store channels.\n(See log for details.)",
                            )
                    # Definitively invalid: mark as invalid and clear channels
                    elif result is False:
                        try:
                            with sqlite3.connect(DB_PATH) as conn:
                                cur = conn.cursor()
                                cur.execute(
                                    """
                                    UPDATE servers
                                    SET last_checked = ?, is_valid = 0
                                    WHERE address = ? AND username = ? AND password = ?
                                    """,
                                    (int(time.time()), addr, user, pw),
                                )
                                cur.execute(
                                    """
                                    DELETE FROM channels
                                    WHERE address = ? AND username = ? AND password = ?
                                    """,
                                    (addr, user, pw),
                                )
                                conn.commit()
                            xbmcgui.Dialog().ok(
                                "Update Server",
                                "Server failed validation and was marked invalid.\nAll its channels were removed.",
                            )
                        except Exception as e:
                            xbmc.log(f"[file_iptv] Update server invalidate error {addr}: {e}", xbmc.LOGERROR)
                            xbmcgui.Dialog().ok(
                                "Update Server",
                                f"Error while marking server invalid: {e}",
                            )
                    # Transient error
                    else:
                        xbmcgui.Dialog().ok(
                            "Update Server",
                            "Transient error while updating this server.\n(See log for details.)",
                        )
                    continue

                # Delete the selected server and its channels
                if action == 2:
                    confirm_msg = (
                        f"Delete server:\n{addr}\n({user})?\n"
                        "This removes all its channels too."
                    )
                    if not xbmcgui.Dialog().yesno("Delete Server", confirm_msg):
                        continue

                    try:
                        with sqlite3.connect(DB_PATH) as conn:
                            cur = conn.cursor()
                            cur.execute("""
                                DELETE FROM channels
                                WHERE address = ? AND username = ? AND password = ?
                            """, (addr, user, pw))
                            cur.execute("""
                                DELETE FROM servers
                                WHERE address = ? AND username = ? AND password = ?
                            """, (addr, user, pw))
                            conn.commit()

                        xbmc.log(f"[file_iptv] Deleted server: {addr}", xbmc.LOGINFO)
                        del server_list[idx]
                        del servers[idx]
                        xbmcgui.Dialog().ok("Delete Server", f"Deleted {addr} and its channels.")
                        if not server_list:
                            break
                    except Exception as e:
                        xbmc.log(f"[file_iptv] Delete error {addr}: {e}", xbmc.LOGERROR)
                        xbmcgui.Dialog().ok("Delete Server", f"Error: {e}")

                # Toggle whether this server participates in search
                if action == 3:
                    try:
                        with sqlite3.connect(DB_PATH) as conn:
                            cur = conn.cursor()
                            cur.execute(
                                """
                                UPDATE servers
                                SET search_enabled = CASE COALESCE(search_enabled, 1)
                                                        WHEN 1 THEN 0 ELSE 1 END
                                WHERE address = ? AND username = ? AND password = ?
                                """,
                                (addr, user, pw),
                            )
                            conn.commit()

                        # Reflect change in local list/labels
                        current_flag = 1 if selected_server.get("search_enabled", 1) else 0
                        new_flag = 0 if current_flag else 1
                        selected_server["search_enabled"] = new_flag
                        visible = "[COLORgreen]ON[/COLOR]" if new_flag else "[COLORred]OFF[/COLOR]"
                        ts = time.strftime(
                            "%Y-%m-%d %H:%M",
                            time.localtime(selected_server["last_checked"]),
                        ) if selected_server["last_checked"] else "Never"
                        server_list[idx] = f"[{visible}] {addr} ({user[:8]}...) - {ts}"
                    except Exception as e:
                        xbmc.log(f"[file_iptv] Toggle search visibility error {addr}: {e}", xbmc.LOGERROR)
                        xbmcgui.Dialog().ok("Server action", f"Error toggling visibility: {e}")

            xbmcgui.Dialog().ok("Manage Servers", "Done managing servers.")

        @plugin.route(f"/{self.name}/settings")
        def settings():
            """Simple UI to tweak file_iptv settings and clear history."""
            dlg = xbmcgui.Dialog()

            def _normalize_list(raw):
                if isinstance(raw, str):
                    return [p.strip() for p in raw.split(",") if p.strip()]
                if isinstance(raw, list):
                    return [str(p).strip() for p in raw if str(p).strip()]
                return []

            def _preview_list(values, max_items=3):
                values = _normalize_list(values)
                if not values:
                    return "[COLORgrey]None[/COLOR]"
                shown = ", ".join(values[:max_items])
                if len(values) > max_items:
                    shown += f" [COLORgrey](+{len(values) - max_items})[/COLOR]"
                return f"[COLORaqua]{shown}[/COLOR]"

            def _manage_text_list(config_key, menu_title, add_prompt):
                values = _normalize_list(config.get(config_key, []))
                while True:
                    labels = list(values) + ["[Add new]", "[Back]"]
                    sel = dlg.select(menu_title, labels)
                    if sel in (-1, len(labels) - 1):
                        break

                    if sel == len(labels) - 2:
                        entered = dlg.input(add_prompt, type=xbmcgui.INPUT_ALPHANUM)
                        if entered:
                            parts = [p.strip() for p in entered.split(",") if p.strip()]
                            existing = {v.lower() for v in values}
                            changed = False
                            for part in parts:
                                key = part.lower()
                                if key not in existing:
                                    existing.add(key)
                                    values.append(part)
                                    changed = True
                            if changed:
                                config[config_key] = values
                                save_config()
                        continue

                    if 0 <= sel < len(values):
                        action = dlg.select(
                            "Entry action",
                            ["Remove entry", "Edit entry", "Back"],
                        )
                        if action == 0:
                            del values[sel]
                            config[config_key] = values
                            save_config()
                        elif action == 1:
                            edited = dlg.input(
                                "Edit value",
                                defaultt=values[sel],
                                type=xbmcgui.INPUT_ALPHANUM,
                            )
                            if edited:
                                values[sel] = edited.strip()
                                deduped = []
                                seen = set()
                                for v in values:
                                    lk = v.lower()
                                    if lk in seen:
                                        continue
                                    seen.add(lk)
                                    deduped.append(v)
                                values = deduped
                                config[config_key] = values
                                save_config()

            while True:
                total_limit = config.get("total_result_limit", 1000)
                per_addr = config.get("per_address_limit", 1000)
                servers_txt = config.get("servers_txt", "")
                recheck_hours = config.get("recheck_hours", 12)
                min_conn_required = config.get("min_connections", 3)
                max_servers = config.get("max_servers_per_update", 10)
                stream_check_enabled = bool(config.get("stream_check_enabled", True))
                stream_candidates = config.get("stream_check_candidates", 5)
                proxy_mode = config.get("proxy_mode", "auto")
                auto_update_enabled = bool(config.get("auto_update_enabled", False))
                debug_logging = bool(config.get("debug_logging", False))
                tk = config.get("test_channel_keywords", [])
                if isinstance(tk, str):
                    keywords_str = tk
                elif isinstance(tk, list):
                    keywords_str = ", ".join(str(x) for x in tk)
                else:
                    keywords_str = ""
                exclude_groups_preview = _preview_list(config.get("exclude_groups", []))
                exclude_names_preview = _preview_list(config.get("exclude_names", []))

                # Mask the Servers TXT value in the menu so the full URL/path
                # is not exposed; still use the real value when editing.
                if servers_txt:
                    masked_servers_txt = "." * max(8, min(len(servers_txt), 24))
                else:
                    masked_servers_txt = ""
                
                choices = [
                    f"Total results per search: [B][COLORyellow]{total_limit}[/COLOR][/B]",
                    f"Per-server result cap: [B][COLORlime]{per_addr}[/COLOR][/B]",
                    f"Servers TXT: [COLORgrey]{masked_servers_txt}[/COLOR]",
                    "Reset Servers TXT to built-in default",
                    f"Server re-check interval: [B][COLORyellow]{recheck_hours}[/COLOR][/B] hours",
                    f"Minimum connections per server: [B][COLORlime]{min_conn_required}[/COLOR][/B]",
                    f"Servers to validate per update: [B][COLORyellow]{max_servers}[/COLOR][/B]",
                    f"Stream check: [B]{'[COLORgreen]Enabled[/COLOR]' if stream_check_enabled else '[COLORred]Disabled[/COLOR]'}[/B]",
                    f"Stream check candidates: [B][COLORyellow]{stream_candidates}[/COLOR][/B] channels/server",
                    f"Preferred test channel names: [COLORaqua]{keywords_str}[/COLOR]",
                    f"Excluded groups: {exclude_groups_preview}",
                    f"Excluded names: {exclude_names_preview}",
                    f"Proxy mode: [B][COLORyellow]{proxy_mode}[/COLOR][/B]",
                    f"Auto-update in background: [B]{'[COLORgreen]Enabled[/COLOR]' if auto_update_enabled else '[COLORred]Disabled[/COLOR]'}[/B]",
                    "Manage M3U sources",
                    f"Debug logging: [B]{'[COLORgreen]On[/COLOR]' if debug_logging else '[COLORred]Off[/COLOR]'}[/B]",
                    "Update file servers now",
                    "Manage file servers",
                    "Reset IPTV database (clear DB file)",
                    "View recent searches",
                    "Clear recent searches",
                    "Close",
                ]

                idx = dlg.select("File IPTV Settings", choices)
                if idx in (-1, len(choices) - 1):
                    break

                # Change total_result_limit
                if idx == 0:
                    val = dlg.input(
                        "Total results per search",
                        defaultt=str(total_limit),
                        type=xbmcgui.INPUT_NUMERIC,
                    )
                    if val and val.isdigit() and int(val) > 0:
                        config["total_result_limit"] = int(val)
                        save_config()

                # Change per_address_limit
                elif idx == 1:
                    val = dlg.input(
                        "Per-server result cap",
                        defaultt=str(per_addr),
                        type=xbmcgui.INPUT_NUMERIC,
                    )
                    if val and val.isdigit() and int(val) > 0:
                        config["per_address_limit"] = int(val)
                        save_config()

                # Change servers_txt path/URL
                elif idx == 2:
                    # Do not reveal the existing value; start blank and
                    # treat empty input as "no change". Also hide typed
                    # characters where supported.
                    try:
                        val = dlg.input(
                            "Servers TXT (URL or path)",
                            defaultt="",
                            type=xbmcgui.INPUT_ALPHANUM,
                            option=xbmcgui.ALPHANUM_HIDE_INPUT,
                        )
                    except TypeError:
                        # Fallback for older Kodi without option param
                        val = dlg.input(
                            "Servers TXT (URL or path)",
                            defaultt="",
                            type=xbmcgui.INPUT_ALPHANUM,
                        )

                    if val:
                        config["servers_txt"] = val.strip()
                        save_config()

                # Reset servers_txt back to the built-in default value
                elif idx == 3:
                    default_txt = DEFAULT_CONFIG.get("servers_txt", "").strip()
                    if not default_txt:
                        dlg.ok("Reset Servers TXT", "No built-in default TXT is defined.")
                    else:
                        if dlg.yesno(
                            "Reset Servers TXT",
                            "Reset Servers TXT URL/path to the built-in default?",
                        ):
                            config["servers_txt"] = default_txt
                            save_config()

                # Change recheck interval (hours)
                elif idx == 4:
                    val = dlg.input(
                        "Server re-check interval (hours)",
                        defaultt=str(recheck_hours),
                        type=xbmcgui.INPUT_NUMERIC,
                    )
                    if val and val.isdigit() and int(val) > 0:
                        config["recheck_hours"] = int(val)
                        save_config()

                # Minimum connections required per server
                elif idx == 5:
                    val = dlg.input(
                        "Minimum connections per server",
                        defaultt=str(min_conn_required),
                        type=xbmcgui.INPUT_NUMERIC,
                    )
                    if val and val.isdigit():
                        config["min_connections"] = int(val)
                        save_config()

                # Max servers per update run (affects validation LIMIT)
                                # Max servers per update run (affects validation LIMIT)
                elif idx == 6:
                    val = dlg.input(
                        "Servers to validate per update\n(0 = disable Xtream servers, use M3U only)",
                        defaultt=str(max_servers),
                        type=xbmcgui.INPUT_NUMERIC,
                    )
                    if val and val.isdigit():
                        new_val = int(val)
                        if new_val >= 0:   # Allow 0 now
                            config["max_servers_per_update"] = new_val
                            save_config()
                        else:
                            xbmcgui.Dialog().ok("Invalid Value", "Value must be 0 or higher.")

                # Toggle stream check on/off
                elif idx == 7:
                    config["stream_check_enabled"] = not stream_check_enabled
                    save_config()

                # Number of candidate channels to test per server
                elif idx == 8:
                    val = dlg.input(
                        "Stream check candidates per server",
                        defaultt=str(stream_candidates),
                        type=xbmcgui.INPUT_NUMERIC,
                    )
                    if val and val.isdigit() and int(val) > 0:
                        config["stream_check_candidates"] = int(val)
                        save_config()

                # Preferred test channel keywords
                elif idx == 9:
                    val = dlg.input(
                        "Preferred test channel names (comma-separated)",
                        defaultt=keywords_str,
                        type=xbmcgui.INPUT_ALPHANUM,
                    )
                    if val:
                        parts = [p.strip() for p in val.split(",") if p.strip()]
                        if parts:
                            config["test_channel_keywords"] = parts
                            save_config()

                # Manage exclude_groups list
                elif idx == 10:
                    _manage_text_list(
                        "exclude_groups",
                        "Excluded groups",
                        "Add group text (comma-separated allowed)",
                    )

                # Manage exclude_names list
                elif idx == 11:
                    _manage_text_list(
                        "exclude_names",
                        "Excluded names",
                        "Add name text (comma-separated allowed)",
                    )

                # Proxy mode: auto, jetproxy, or direct
                elif idx == 12:
                    options = [
                        "auto (jetproxy for servers, direct for unknown)",
                        "force jetproxy",
                        "force direct",
                    ]
                    current = config.get("proxy_mode", "auto")
                    try:
                        cur_idx = {"auto": 0, "jetproxy": 1, "direct": 2}.get(current, 0)
                    except Exception:
                        cur_idx = 0
                    sel = dlg.select("Proxy mode", options)
                    if sel in (0, 1, 2):
                        config["proxy_mode"] = ["auto", "jetproxy", "direct"][sel]
                        save_config()

                # Toggle background auto-update
                elif idx == 13:
                    config["auto_update_enabled"] = not auto_update_enabled
                    save_config()

                # Manage generic M3U sources list
                elif idx == 14:
                    sources = load_m3u_sources()
                    while True:
                        labels = list(sources) + ["[Add new]", "[Back]"]
                        sel = dlg.select("M3U Sources", labels)
                        if sel in (-1, len(labels) - 1):
                            break
                        # Add new source
                        if sel == len(labels) - 2:
                            val = dlg.input(
                                "Add M3U source URL or path",
                                type=xbmcgui.INPUT_ALPHANUM,
                            )
                            if val:
                                new_src = val.strip()
                                if new_src and new_src not in sources:
                                    sources.append(new_src)
                                    config["m3u_sources"] = sources
                                    save_config()
                            continue
                        # Remove existing source
                        if 0 <= sel < len(sources):
                            to_remove = sources[sel]
                            if dlg.yesno("Remove M3U source", f"Delete:\n{to_remove} ?"):
                                del sources[sel]
                                config["m3u_sources"] = sources
                                save_config()

                # Toggle debug logging
                elif idx == 15:
                    config["debug_logging"] = not debug_logging
                    save_config()

                # Update servers now (same as /file_iptv/update)
                elif idx == 16:
                    update()

                # Manage servers list (same as /file_iptv/manage_servers)
                elif idx == 17:
                    manage_servers()

                # Reset the IPTV database file (same as /file_iptv/reset)
                elif idx == 18:
                    reset()

                # View recent searches (read-only list)
                elif idx == 19:
                    history = load_recent_searches()
                    if not history:
                        dlg.ok("Recent IPTV", "No recent searches yet.")
                    else:
                        labels = []
                        for h in history:
                            term = h.get("term", "")
                            country = h.get("country", "") or "Any"
                            ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(h.get("time", 0)))
                            labels.append(f"[B]{term}[/B]  [COLORyellow]({country})[/COLOR]\n[COLORgrey]{ts}[/COLOR]")
                        # Ignore the return value; this is just for
                        # inspection, not for re-running searches.
                        link_dialog(labels, return_idx=False, hide_links=False)

                # Clear recent search history
                elif idx == 20:
                    if dlg.yesno("Recent IPTV", "Clear recent search history?"):
                        try:
                            if os.path.exists(RECENT_PATH):
                                os.remove(RECENT_PATH)
                            dlg.ok("Recent IPTV", "Recent searches cleared.")
                        except Exception as e:
                            xbmc.log(f"[file_iptv] Failed to clear recent: {e}", xbmc.LOGERROR)
                            dlg.ok("Recent IPTV", f"Error clearing history: {e}")

        # Workaround for: No route to path "/https://magnetic.website/MAD_TITAN_SPORTS/TOOLS/tools.json"
        @plugin.route("/https://magnetic.website/MAD_TITAN_SPORTS/TOOLS/tools.json")
        def tools_root_redirect():
            """
            Redirect legacy entry point
            plugin://plugin.video.madtitansports/https://.../TOOLS/tools.json
            to the existing message/ route that the addon already handles.
            """
            # Hand off to the core handler so this request returns proper directory items
            plugin.redirect(
                "/message/https://magnetic.website/MAD_TITAN_SPORTS/TOOLS/tools.json"
            )

    def update_servers(self, show_progress=True, show_summary=True):
        """Load servers from TXT, validate, and update database with a fixed limit, always filling with valid servers. Scans entire TXT if needed."""

        dialog = xbmcgui.DialogProgress() if show_progress else None
        if dialog:
            dialog.create("IPTV Update", "Loading servers from TXT...")

        val = config.get("max_servers_per_update", 50)
        max_servers = int(val) if val is not None else 50

        if max_servers == 0:
            xbmc.log("[file_iptv] max_servers_per_update=0 → Skipping all Xtream servers (M3U only mode)", xbmc.LOGINFO)
            with sqlite3.connect(DB_PATH) as conn:
                cur = conn.cursor()
                cur.execute(
                    """
                    DELETE FROM channels
                    WHERE address IN (
                        SELECT address FROM servers WHERE username <> ?
                    )
                    """,
                    (M3U_USERNAME,),
                )
                cur.execute(
                    """
                    DELETE FROM servers
                    WHERE username <> ?
                    """,
                    (M3U_USERNAME,),
                )
                conn.commit()
            if show_summary:
                xbmcgui.Dialog().ok("IPTV Update", "M3U-Only Mode Enabled.\nXtream servers disabled.")
            update_m3u_playlists()
            return {
                "checked": 0,
                "validated": 0,
                "invalidated": 0,
                "transient": 0,
            }
        servers_added = 0
        servers_removed = 0
        servers_invalidated = 0
        servers_transient = 0
        processed = 0
        fallback_mode = False

        # Get current non-M3U servers and trim overflow inside the same DB session.
        with sqlite3.connect(DB_PATH) as conn:
            cur = conn.cursor()
            cur.execute(
                "SELECT address, username, password, is_valid FROM servers WHERE username <> ?",
                (M3U_USERNAME,),
            )
            db_servers = cur.fetchall()
            valid_servers = [s for s in db_servers if s[3] == 1]

            if len(valid_servers) > max_servers:
                to_remove = valid_servers[max_servers:]
                for s in to_remove:
                    cur.execute(
                        "DELETE FROM channels WHERE address = ? AND username = ? AND password = ?",
                        (s[0], s[1], s[2]),
                    )
                    cur.execute(
                        "DELETE FROM servers WHERE address = ? AND username = ? AND password = ?",
                        (s[0], s[1], s[2]),
                    )
                    servers_removed += 1
                conn.commit()
                valid_servers = valid_servers[:max_servers]

        validated = len(valid_servers)

        # Read the entire TXT file for candidates.
        all_candidates = []
        total_lines = 0
        parsed_ok = 0
        duplicate_candidates = 0
        skipped_non_xtream = 0
        path = config.get("servers_txt", "").strip()
        try:
            if path.startswith(("http://", "https://")):
                resp = requests.get(path, timeout=10, headers=HTTP_HEADERS)
                resp.raise_for_status()
                lines = resp.text.splitlines()
            else:
                local_path = translatePath(path)
                if not os.path.exists(local_path):
                    xbmc.log(f"[file_iptv] TXT file not found: {local_path}", xbmc.LOGWARNING)
                    lines = []
                else:
                    with open(local_path, "r", encoding="utf-8") as f:
                        lines = f.readlines()

            seen_candidates = set()
            total_lines = len(lines)
            for line in lines:
                cred = parse_server_url(line)
                if cred:
                    parsed_ok += 1
                    if cred not in seen_candidates:
                        seen_candidates.add(cred)
                        all_candidates.append(cred)
                    else:
                        duplicate_candidates += 1
                else:
                    raw = (line or "").strip()
                    if raw and not raw.startswith("#"):
                        skipped_non_xtream += 1
        except Exception as e:
            xbmc.log(f"[file_iptv] Error loading TXT: {e}", xbmc.LOGERROR)
            all_candidates = []

        total_to_process = len(valid_servers) + len(all_candidates)
        xbmc.log(
            (
                f"[file_iptv] TXT stats: lines={total_lines}, parsed={parsed_ok}, "
                f"unique={len(all_candidates)}, duplicates={duplicate_candidates}, "
                f"non_xtream_or_invalid={skipped_non_xtream}"
            ),
            xbmc.LOGINFO,
        )

        # Track what we have already handled this run.
        attempted = set()

        # Re-validate existing valid servers.
        for s in list(valid_servers):
            # Do not abort mid-run based on dialog cancel state; users were
            # seeing incomplete scans when the progress dialog was dismissed.

            processed += 1
            server = (s[0], s[1], s[2])
            attempted.add(server)

            masked_user = server[1][:8] + "..." if len(server[1]) > 8 else server[1]
            if dialog:
                percent = min(100, int((processed / max(1, total_to_process)) * 100))
                dialog.update(
                    percent,
                    f"Re-validating existing servers ({processed}/{total_to_process})\nVerifying: {server[0]} ({masked_user})",
                )

                        # Smart validation: skip stream check for recently good servers
            last_checked = s[3] if len(s) > 3 else 0
            hours_since_check = (int(time.time()) - last_checked) / 3600 if last_checked else 999

            stream_check_override = None
            if hours_since_check < config.get("recheck_hours", 12):
                stream_check_override = False
                xbmc.log(f"[file_iptv] Skipping stream check for known good: {server[0]}", xbmc.LOGDEBUG)

            result = validate_server(server, stream_check_override=stream_check_override)
            
            if isinstance(result, tuple):
                panel_data, max_conn = result
                store_server_channels(server, panel_data, max_conn)
                status = "OK"
            elif result is False:
                try:
                    with sqlite3.connect(DB_PATH) as conn:
                        cur = conn.cursor()
                        cur.execute(
                            "DELETE FROM channels WHERE address = ? AND username = ? AND password = ?",
                            (server[0], server[1], server[2]),
                        )
                        cur.execute(
                            "DELETE FROM servers WHERE address = ? AND username = ? AND password = ?",
                            (server[0], server[1], server[2]),
                        )
                        conn.commit()
                    validated = max(0, validated - 1)
                    servers_removed += 1
                    servers_invalidated += 1
                    status = "Removed (Invalid)"
                except Exception as e:
                    xbmc.log(f"[file_iptv] Failed to remove stale server: {e}", xbmc.LOGERROR)
                    status = "Remove failed"
            else:
                servers_transient += 1
                status = "Transient error"

            if dialog:
                dialog.update(
                    percent,
                    f"Re-validating existing servers ({processed}/{total_to_process})\n{server[0]} ({masked_user}) - {status}",
                )

        # Validate candidates until we fill max_servers or exhaust all candidates.
        idx = 0
        while validated < max_servers and idx < len(all_candidates):
            # Keep scanning candidates even if progress dialog is dismissed.

            server = all_candidates[idx]
            idx += 1

            if server in attempted:
                continue

            processed += 1
            attempted.add(server)
            masked_user = server[1][:8] + "..." if len(server[1]) > 8 else server[1]

            if dialog:
                percent = min(100, int((processed / max(1, total_to_process)) * 100))
                dialog.update(
                    percent,
                    f"Validating new servers ({processed}/{total_to_process})\nVerifying: {server[0]} ({masked_user})",
                )

            xbmc.log(f"[file_iptv] Validating server candidate: {server[0]} {server[1]}", xbmc.LOGINFO)

            # New servers always do full check on first validation
            result = validate_server(server)

            if isinstance(result, tuple):
                panel_data, max_conn = result
                store_server_channels(server, panel_data, max_conn)
                validated += 1
                servers_added += 1
                status = "Added"
            elif result is False:
                try:
                    with sqlite3.connect(DB_PATH) as conn:
                        cur = conn.cursor()
                        cur.execute(
                            "DELETE FROM channels WHERE address = ? AND username = ? AND password = ?",
                            (server[0], server[1], server[2]),
                        )
                        cur.execute(
                            "DELETE FROM servers WHERE address = ? AND username = ? AND password = ?",
                            (server[0], server[1], server[2]),
                        )
                        conn.commit()
                    servers_removed += 1
                    servers_invalidated += 1
                    status = "Invalid"
                except Exception as e:
                    xbmc.log(f"[file_iptv] Failed to remove invalid server: {e}", xbmc.LOGERROR)
                    status = "Invalid (remove failed)"
            else:
                servers_transient += 1
                status = "Transient error"

            if dialog:
                dialog.update(
                    percent,
                    f"Validating new servers ({processed}/{total_to_process})\n{server[0]} ({masked_user}) - {status}",
                )

        # Fallback pass: if strict pass found very few servers, retry remaining
        # candidates with relaxed rules to recover behavior closer to older
        # versions (lower min connections and no stream probe).
        try:
            strict_min_conn = int(config.get("min_connections", 2) or 0)
        except Exception:
            strict_min_conn = 2
        strict_stream = bool(config.get("stream_check_enabled", True))

        should_fallback = (
            validated < max_servers
            and strict_min_conn > 1
            and strict_stream
        )

        if should_fallback:
            fallback_mode = True
            xbmc.log(
                "[file_iptv] Entering fallback validation pass (min_connections=1, stream_check=False)",
                xbmc.LOGINFO,
            )

            for server in all_candidates:
                if validated >= max_servers:
                    break
                if server in attempted:
                    continue

                processed += 1
                attempted.add(server)
                masked_user = server[1][:8] + "..." if len(server[1]) > 8 else server[1]

                if dialog:
                    percent = min(100, int((processed / max(1, total_to_process)) * 100))
                    dialog.update(
                        percent,
                        f"Fallback validation ({processed}/{total_to_process})\nVerifying: {server[0]} ({masked_user})",
                    )

                result = validate_server(server, min_conn_override=1, stream_check_override=False)
                if isinstance(result, tuple):
                    panel_data, max_conn = result
                    store_server_channels(server, panel_data, max_conn)
                    validated += 1
                    servers_added += 1
                    status = "Added (fallback)"
                elif result is False:
                    try:
                        with sqlite3.connect(DB_PATH) as conn:
                            cur = conn.cursor()
                            cur.execute(
                                "DELETE FROM channels WHERE address = ? AND username = ? AND password = ?",
                                (server[0], server[1], server[2]),
                            )
                            cur.execute(
                                "DELETE FROM servers WHERE address = ? AND username = ? AND password = ?",
                                (server[0], server[1], server[2]),
                            )
                            conn.commit()
                        servers_removed += 1
                        servers_invalidated += 1
                        status = "Invalid (fallback)"
                    except Exception as e:
                        xbmc.log(f"[file_iptv] Failed to remove invalid server: {e}", xbmc.LOGERROR)
                        status = "Invalid (remove failed)"
                else:
                    servers_transient += 1
                    status = "Transient error (fallback)"

                if dialog:
                    dialog.update(
                        percent,
                        f"Fallback validation ({processed}/{total_to_process})\n{server[0]} ({masked_user}) - {status}",
                    )

        if dialog:
            dialog.update(100, "Update complete")
            dialog.close()

        summary = [
            "Update complete!",
            f"Servers checked: {processed}",
            f"TXT unique candidates: {len(all_candidates)}",
            f"Valid servers now: {validated} / {max_servers}",
            f"New servers added: {servers_added}",
            f"Servers invalidated/removed: {servers_invalidated}",
            f"Transient errors: {servers_transient}",
            f"Fallback pass used: {'Yes' if fallback_mode else 'No'}",
            f"Stopped because: {'Target reached' if validated >= max_servers else 'Exhausted candidate list'}",
        ]

        # Mirror final summary to Kodi log for troubleshooting.
        for line in summary:
            xbmc.log(f"[file_iptv] {line}", xbmc.LOGINFO)

        if show_summary:
            xbmcgui.Dialog().ok("IPTV Update", "\n".join(summary))
                # Always update M3U playlists too
            update_m3u_playlists()
            return {
                "checked": processed,
                "validated": validated,
                "invalidated": servers_invalidated,
                "transient": servers_transient,
            }
