# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import json
import re
import time
import uuid
import hashlib
import os
import base64
from datetime import datetime, timedelta, timezone

import requests

try:
    from zoneinfo import ZoneInfo
except Exception:
    ZoneInfo = None
import xbmc
import xbmcgui
from resources.lib.common import log, safe_text, make_session
from resources.lib import cache_store

try:
    from urllib.parse import quote_plus
except ImportError:
    from urllib import quote_plus

USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
WINDOW_ID = 10000
PLUTO_TTL_SECONDS = None
PLUTO_CHANNELS_TTL_SECONDS = 900
PLUTO_EPG_TTL_SECONDS = 60
PLUTO_BOOT_TTL_SECONDS = 240
PLUTO_VOD_TTL_SECONDS = 1800
CACHE_MANIFEST_PROP = 'oneplay.pluto.cache.manifest'
SESSION = make_session(user_agent=USER_AGENT, retries=1)
PLUTO_PERSISTENT_NAMESPACE = 'pluto_public_navigation_v1'
PLUTO_PERSISTENT_STALE_SECONDS = 3 * 86400


def _log_warning(message):
    log('[Pluto] {}'.format(safe_text(message)), level=getattr(xbmc, 'LOGWARNING', 2))


def _log_error(message):
    log('[Pluto] {}'.format(safe_text(message)), level=getattr(xbmc, 'LOGERROR', 4))

SERVICE_CHANNELS_URL = 'https://service-channels.clusters.pluto.tv/v2/guide/channels'
SERVICE_CATEGORIES_URL = 'https://service-channels.clusters.pluto.tv/v2/guide/categories'
SERVICE_TIMELINES_URL = 'https://service-channels.clusters.pluto.tv/v2/guide/timelines'
LEGACY_CHANNELS_URL = 'https://api.pluto.tv/v2/channels.json'
LEGACY_GUIDE_URL = 'https://api.pluto.tv/v2/channels'
VOD_CATEGORIES_URL = 'https://api.pluto.tv/v3/vod/categories?includeItems=true&deviceType=web'
VOD_SERIES_URL = 'https://api.pluto.tv/v3/vod/series/{series_id}/seasons?includeItems=true&deviceType=web'
STITCHER_FALLBACK = 'https://cfd-v4-service-channel-stitcher-use1-1.prd.pluto.tv'

COUNTRY_CONFIG = {
    'auto': {'label': 'Selecionar ao abrir', 'ips': [], 'tz_name': ''},
    'br': {'label': 'Brasil', 'ips': ['177.47.27.205', '177.200.80.12', '187.111.144.26'], 'tz_name': 'America/Sao_Paulo'},
    'us': {'label': 'Estados Unidos', 'ips': ['185.236.200.172', '23.227.38.2', '64.237.32.10'], 'tz_name': 'America/New_York'},
    'mx': {'label': 'México', 'ips': ['200.68.128.83', '201.150.35.2', '189.203.240.12'], 'tz_name': 'America/Mexico_City'},
    'es': {'label': 'Espanha', 'ips': ['88.26.241.248', '81.34.122.10', '95.60.14.9'], 'tz_name': 'Europe/Madrid'},
    'fr': {'label': 'França', 'ips': ['176.31.84.249', '51.158.112.203', '90.84.146.10'], 'tz_name': 'Europe/Paris'},
    'it': {'label': 'Itália', 'ips': ['5.133.48.0', '79.7.120.14', '93.62.151.2'], 'tz_name': 'Europe/Rome'},
    'de': {'label': 'Alemanha', 'ips': ['85.214.132.117', '91.64.44.2', '87.123.10.8'], 'tz_name': 'Europe/Berlin'},
    'gb': {'label': 'Reino Unido', 'ips': ['185.199.220.58', '51.140.12.8', '81.129.55.13'], 'tz_name': 'Europe/London'},
    'ca': {'label': 'Canadá', 'ips': ['192.206.151.131', '24.114.4.9', '142.167.18.21'], 'tz_name': 'America/Toronto'},
    'au': {'label': 'Austrália', 'ips': ['144.48.37.140', '203.10.55.12', '101.0.85.4'], 'tz_name': 'Australia/Sydney'},
    'ar': {'label': 'Argentina', 'ips': ['104.103.238.0', '186.33.231.9', '190.12.101.3'], 'tz_name': 'America/Argentina/Buenos_Aires'},
    'co': {'label': 'Colômbia', 'ips': ['181.204.4.74', '181.49.120.8', '190.242.116.4'], 'tz_name': 'America/Bogota'},
    'cl': {'label': 'Chile', 'ips': ['161.238.0.0', '190.96.83.10', '200.73.77.12'], 'tz_name': 'America/Santiago'},
}
COUNTRY_ORDER = ['br', 'us', 'mx', 'es', 'fr', 'it', 'de', 'gb', 'ca', 'au', 'ar', 'co', 'cl']


def _window():
    return xbmcgui.Window(WINDOW_ID)


def new_session_id():
    try:
        return base64.urlsafe_b64encode(os.urandom(9)).decode('ascii').rstrip('=')
    except Exception:
        return str(os.times())


def get_session_id():
    try:
        return _window().getProperty('oneplay.pluto.session_id') or ''
    except Exception:
        return ''


def set_session_id(session_id):
    try:
        _window().setProperty('oneplay.pluto.session_id', session_id or '')
    except Exception:
        pass


def clear_session_id():
    try:
        _window().clearProperty('oneplay.pluto.session_id')
    except Exception:
        pass


def ensure_session(session_id=''):
    if isinstance(session_id, dict):
        session_id = session_id.get('psid', '')
    incoming = str(session_id or '').strip()
    current = get_session_id()
    if not incoming:
        incoming = new_session_id()
    if incoming != current:
        clear_session_cache()
        set_session_id(incoming)
    return incoming


def _register_cache_key(key):
    try:
        win = _window()
        prop = _cache_prop_name(key)
        manifest_raw = win.getProperty(CACHE_MANIFEST_PROP)
        manifest = []
        if manifest_raw:
            try:
                manifest = json.loads(manifest_raw)
            except Exception:
                manifest = []
        if prop not in manifest:
            manifest.append(prop)
            win.setProperty(CACHE_MANIFEST_PROP, json.dumps(manifest, separators=(',', ':')))
    except Exception:
        pass


def _cache_prop_name(key):
    try:
        digest = hashlib.md5(repr(key).encode('utf-8')).hexdigest()
    except Exception:
        digest = hashlib.md5(str(key).encode('utf-8')).hexdigest()
    return 'oneplay.pluto.{}'.format(digest)


def _persistent_public_key(key):
    """Somente dados públicos de navegação entram no SQLite persistente.

    Tokens de boot, sessão e URLs temporárias de stream continuam apenas na
    memória da janela Kodi. Isso evita reutilizar autenticação ou stream já
    expirado após reiniciar o app.
    """
    try:
        kind = key[0] if isinstance(key, (list, tuple)) and key else ''
    except Exception:
        kind = ''
    if kind in ('service_channels', 'vod_categories', 'vod_categories_filtered', 'vod_items', 'vod_series'):
        return True
    if kind == 'legacy_channels':
        try:
            return not bool(key[2])
        except Exception:
            return False
    if kind == 'channels':
        try:
            return not bool(key[2])
        except Exception:
            return False
    return False


def _persistent_key_name(key):
    try:
        return cache_store.make_key(repr(key))
    except Exception:
        return cache_store.make_key(str(key))


def _persistent_get_record(key, allow_stale=False):
    if not _persistent_public_key(key):
        return None
    try:
        record = cache_store.get_record(PLUTO_PERSISTENT_NAMESPACE, _persistent_key_name(key))
        if not record:
            return None
        if cache_store.is_fresh(record):
            return record
        if allow_stale:
            age = int(time.time()) - int(record.get('updated_at') or 0)
            if age <= PLUTO_PERSISTENT_STALE_SECONDS:
                return record
    except Exception:
        pass
    return None


def _persistent_set(key, value, ttl):
    if not _persistent_public_key(key):
        return
    try:
        cache_store.set_record(PLUTO_PERSISTENT_NAMESPACE, _persistent_key_name(key), value, ttl)
    except Exception:
        pass


def _cache_get_payload(key, ignore_ttl=False, clear_expired=True):
    try:
        raw = _window().getProperty(_cache_prop_name(key))
        if raw:
            data = json.loads(raw)
            if ignore_ttl:
                return data
            ttl = data.get('ttl', PLUTO_TTL_SECONDS)
            if ttl not in (None, '', False):
                ttl = float(ttl)
                if time.time() - float(data.get('ts', 0)) > ttl:
                    if clear_expired:
                        try:
                            _window().clearProperty(_cache_prop_name(key))
                        except Exception:
                            pass
                    data = None
            if data:
                return data
    except Exception:
        pass

    record = _persistent_get_record(key, allow_stale=ignore_ttl)
    if not record:
        return None
    value = record.get('value')
    # Espelha o hit persistente na janela somente até o mesmo vencimento local.
    try:
        now_ts = time.time()
        remaining = max(1, int(record.get('expires_at') or 0) - int(now_ts))
        payload = {'ts': now_ts, 'ttl': remaining, 'value': value}
        _window().setProperty(_cache_prop_name(key), json.dumps(payload, separators=(',', ':')))
        _register_cache_key(key)
        return payload
    except Exception:
        return {'ts': time.time(), 'ttl': 1, 'value': value}


def _cache_get(key):
    data = _cache_get_payload(key, ignore_ttl=False, clear_expired=True)
    if not data:
        return None
    return data.get('value')


def _cache_get_stale(key):
    data = _cache_get_payload(key, ignore_ttl=True, clear_expired=False)
    if not data:
        return None
    return data.get('value')


def _cache_set(key, value, ttl=PLUTO_TTL_SECONDS):
    try:
        payload = json.dumps({'ts': time.time(), 'ttl': ttl, 'value': value}, separators=(',', ':'))
        _window().setProperty(_cache_prop_name(key), payload)
        _register_cache_key(key)
    except Exception:
        pass
    _persistent_set(key, value, ttl)
    return value


def clear_pluto_persistent_cache():
    return cache_store.clear_namespace(PLUTO_PERSISTENT_NAMESPACE)

def clear_session_cache():
    try:
        win = _window()
        manifest_raw = win.getProperty(CACHE_MANIFEST_PROP)
        manifest = []
        if manifest_raw:
            try:
                manifest = json.loads(manifest_raw)
            except Exception:
                manifest = []
        for key in manifest:
            try:
                win.clearProperty(key)
            except Exception:
                pass
        try:
            win.clearProperty(CACHE_MANIFEST_PROP)
        except Exception:
            pass
        clear_session_id()
    except Exception:
        pass


def get_supported_countries():
    return [(code, COUNTRY_CONFIG[code]['label']) for code in ['auto'] + COUNTRY_ORDER]


def country_label(code):
    code = normalize_country_code(code)
    return COUNTRY_CONFIG.get(code, COUNTRY_CONFIG['auto']).get('label', 'Selecionar ao abrir')


def normalize_country_code(code):
    code = str(code or 'auto').strip().lower()
    if code not in COUNTRY_CONFIG:
        return 'auto'
    return code


def _country_ips(country):
    country = normalize_country_code(country)
    ips = COUNTRY_CONFIG.get(country, {}).get('ips', []) or []
    seen = []
    for item in ips:
        value = str(item or '').strip()
        if value and value not in seen:
            seen.append(value)
    return seen


def _country_timezone_name(country):
    country = normalize_country_code(country)
    return str(COUNTRY_CONFIG.get(country, {}).get('tz_name', '') or '').strip()


def _build_geo_candidates(country='auto', include_direct=True):
    country = normalize_country_code(country)
    candidates = []
    seen = set()
    for ip in _country_ips(country):
        key = 'ip:' + ip
        if key in seen:
            continue
        candidates.append({'country': country, 'ip': ip, 'mode': 'spoof'})
        seen.add(key)
    if include_direct or country == 'auto' or not candidates:
        if 'direct' not in seen:
            candidates.append({'country': country, 'ip': '', 'mode': 'direct'})
    return candidates


def _geo_candidate_text(candidate):
    candidate = candidate or {}
    if candidate.get('mode') == 'spoof' and candidate.get('ip'):
        return '{} via {}'.format(country_label(candidate.get('country', 'auto')), candidate.get('ip'))
    return '{} sem spoof'.format(country_label(candidate.get('country', 'auto')))


def _health_cache_key(country):
    return ('pluto_health', normalize_country_code(country))


def _health_update(country, component, ok, detail='', extra=None):
    try:
        payload = _cache_get_stale(_health_cache_key(country)) or {}
        payload[str(component or 'component')] = {
            'ok': bool(ok),
            'detail': safe_text(detail or ''),
            'ts': time.time(),
            'extra': extra or {},
        }
        _cache_set(_health_cache_key(country), payload, ttl=PLUTO_BOOT_TTL_SECONDS)
    except Exception:
        pass


def get_health_snapshot(country='auto'):
    return _cache_get_stale(_health_cache_key(country)) or {}


def expand_country_codes(country='auto', all_countries=False):
    country = normalize_country_code(country)
    if all_countries:
        return list(COUNTRY_ORDER)
    return [country]


def _headers_base():
    return {
        'User-Agent': USER_AGENT,
        'accept': 'application/json, text/javascript, */*; q=0.01',
        'origin': 'https://pluto.tv',
        'referer': 'https://pluto.tv/',
    }


def _apply_geo_headers(headers, candidate=None):
    data = dict(headers or {})
    candidate = candidate or {}
    ip = str(candidate.get('ip', '') or '').strip()
    if ip:
        data['X-Forwarded-For'] = ip
    else:
        data.pop('X-Forwarded-For', None)
    return data


def _legacy_headers(country='auto', candidate=None):
    if candidate is None:
        candidate = {'country': normalize_country_code(country), 'ip': '', 'mode': 'direct'}
    return _apply_geo_headers(_headers_base(), candidate)


def _parse_iso_datetime(value):
    if not value:
        return None
    s = value.strip()
    if s.endswith('Z'):
        s = s[:-1] + '+00:00'
    s = re.sub(r'([+-]\d{2}:\d)(?!\d)', lambda m: m.group(1) + '0', s)
    try:
        dt = datetime.fromisoformat(s)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except Exception:
        for fmt in ('%Y-%m-%dT%H:%M:%S.%f%z', '%Y-%m-%dT%H:%M:%S%z'):
            try:
                return datetime.strptime(s, fmt)
            except Exception:
                continue
    return None


def _epg_timezone(country='auto', timezone_mode='auto'):
    if str(timezone_mode or 'auto').lower() == 'selected_country':
        tz_name = _country_timezone_name(country)
        if tz_name and ZoneInfo is not None:
            try:
                return ZoneInfo(tz_name)
            except Exception:
                pass
    try:
        local_tz = datetime.now().astimezone().tzinfo
        if local_tz is not None:
            return local_tz
    except Exception:
        pass
    return timezone.utc


def _format_country_suffix(country, all_countries=False):
    if not all_countries:
        return ''
    return ' [{}]'.format(country_label(country))


def _fetch_boot_data(country='auto'):
    country = normalize_country_code(country)
    session_id = get_session_id() or ''
    cache_key = ('boot', session_id, country)
    cached = _cache_get(cache_key)
    if cached is not None:
        return cached
    stale = _cache_get_stale(cache_key) or {}
    now_utc = datetime.now(timezone.utc)
    stop_utc = now_utc + timedelta(hours=6)
    params = {
        'appName': 'web',
        'appVersion': '8.0.0-111b2b9dc00bd0bea9030b30662159ed9e7c8bc6',
        'deviceVersion': '122.0.0',
        'deviceModel': 'web',
        'deviceMake': 'chrome',
        'deviceType': 'web',
        'clientID': str(uuid.uuid4()),
        'clientModelNumber': '1.0.0',
        'serverSideAds': 'false',
        'drmCapabilities': 'widevine:L3',
        'blockingMode': '',
        'clientTime': stop_utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
        'lastAppLaunchDate': now_utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
    }
    last_exc = None
    for candidate in _build_geo_candidates(country, include_direct=True):
        headers = _apply_geo_headers(_headers_base(), candidate)
        payload = {
            'sessionToken': '',
            'stitcherParams': '',
            'stitcherUrl': STITCHER_FALLBACK,
            'geo_ip': candidate.get('ip', '') or '',
            'geo_mode': candidate.get('mode', 'direct') or 'direct',
            'country_code': country,
        }
        try:
            response = SESSION.get('https://boot.pluto.tv/v4/start', params=params, headers=headers, timeout=10)
            response.raise_for_status()
            boot_data = response.json() or {}
            payload['sessionToken'] = boot_data.get('sessionToken', '') or ''
            payload['stitcherParams'] = boot_data.get('stitcherParams', '') or ''
            payload['stitcherUrl'] = (boot_data.get('servers', {}) or {}).get('stitcher', STITCHER_FALLBACK) or STITCHER_FALLBACK
            if payload.get('sessionToken') or payload.get('stitcherParams'):
                _health_update(country, 'boot', True, _geo_candidate_text(candidate), extra={'mode': payload.get('geo_mode', 'direct')})
                return _cache_set(cache_key, payload, ttl=PLUTO_BOOT_TTL_SECONDS)
        except Exception as exc:
            last_exc = exc
            _log_warning('Boot falhou para {}: {}'.format(_geo_candidate_text(candidate), exc))
    if stale:
        _health_update(country, 'boot', True, 'usando último boot válido em cache', extra={'mode': stale.get('geo_mode', 'stale')})
        return stale
    payload = {
        'sessionToken': '',
        'stitcherParams': '',
        'stitcherUrl': STITCHER_FALLBACK,
        'geo_ip': '',
        'geo_mode': 'direct',
        'country_code': country,
    }
    _health_update(country, 'boot', False, safe_text(last_exc or 'boot sem token'))
    if last_exc is not None:
        _log_warning('Boot falhou para o país {}: {}'.format(country, last_exc))
    return _cache_set(cache_key, payload, ttl=PLUTO_BOOT_TTL_SECONDS)


def _auth_headers(country='auto'):
    headers = _headers_base()
    boot_data = _fetch_boot_data(country)
    token = boot_data.get('sessionToken', '') or ''
    if token:
        headers['authorization'] = 'Bearer {}'.format(token)
    candidate = {
        'country': normalize_country_code(country),
        'ip': boot_data.get('geo_ip', '') or '',
        'mode': boot_data.get('geo_mode', 'direct') or 'direct',
    }
    return _apply_geo_headers(headers, candidate)


def _extract_category(channel):
    candidates = [
        channel.get('category'),
        channel.get('categoryName'),
        channel.get('genre'),
        channel.get('section'),
    ]
    category_obj = channel.get('category')
    if isinstance(category_obj, dict):
        candidates.extend([category_obj.get('name'), category_obj.get('title'), category_obj.get('slug')])
    categories = channel.get('categories') or []
    if isinstance(categories, list):
        for item in categories:
            if isinstance(item, dict):
                candidates.extend([item.get('name'), item.get('title'), item.get('slug')])
            elif isinstance(item, str):
                candidates.append(item)
    for candidate in candidates:
        if isinstance(candidate, str) and candidate.strip():
            return candidate.strip()
    return 'Geral'


def _window_for_fetch(include_epg):
    now_utc = datetime.now(timezone.utc)
    stop_utc = now_utc + (timedelta(hours=24) if include_epg else timedelta(hours=1))
    return now_utc, stop_utc


def _request_json(url, params=None, headers=None, timeout=12, cache_key=None, stale_ttl=None, label='requisição'):
    stale_value = _cache_get_stale(cache_key) if cache_key is not None else None
    try:
        response = SESSION.get(url, params=params, headers=headers, timeout=timeout)
        response.raise_for_status()
        data = response.json()
        if cache_key is not None and data not in (None, '', []):
            _cache_set(cache_key, data, ttl=stale_ttl)
        return data
    except Exception as exc:
        if stale_value not in (None, '', []):
            _log_warning('{} falhou; reutilizando último payload válido: {}'.format(label, exc))
            return stale_value
        raise


def _channel_key_from_values(slug='', channel_id='', number=0):
    slug = str(slug or '').strip()
    channel_id = str(channel_id or '').strip()
    try:
        number = int(number or 0)
    except Exception:
        number = 0
    if slug:
        return 'slug:' + slug
    if channel_id:
        return 'id:' + channel_id
    return 'num:' + str(number)


def _channel_key(item):
    return _channel_key_from_values(item.get('slug', ''), item.get('channel_id', ''), item.get('number', 0))


def _select_logo(images, fallback=''):
    if isinstance(images, list):
        for item in images:
            if not isinstance(item, dict):
                continue
            if item.get('type') == 'colorLogoPNG' and item.get('url'):
                return item.get('url')
        for item in images:
            if isinstance(item, dict) and item.get('url'):
                return item.get('url')
    return fallback or ''


def _normalize_channel_legacy(channel, country='auto'):
    number = channel.get('number', 0)
    try:
        number_int = int(number)
        if number_int <= 0:
            return None
    except Exception:
        return None
    stitched_urls = channel.get('stitched', {}).get('urls', []) or []
    stitched_url = stitched_urls[0].get('url') if stitched_urls else None
    name = channel.get('name') or '#{}'.format(number_int)
    thumb = channel.get('logo', {}).get('path', '')
    return {
        'number': number_int,
        'name': name,
        'display_name': name,
        'description': '',
        'thumb': thumb,
        'category': _extract_category(channel),
        'stitched_url': stitched_url or '',
        'slug': channel.get('slug', '') or '',
        'channel_id': channel.get('_id', '') or channel.get('id', '') or '',
        'country_code': normalize_country_code(country),
        'country_label': country_label(country),
    }


def _normalize_channel_service(channel, category_lookup, country='auto'):
    try:
        number_int = int(channel.get('number', 0) or 0)
    except Exception:
        number_int = 0
    if number_int <= 0:
        return None
    channel_id = channel.get('id', '') or channel.get('_id', '') or ''
    if not channel_id:
        return None
    name = channel.get('name', '') or '#{}'.format(number_int)
    return {
        'number': number_int,
        'name': name,
        'display_name': name,
        'description': '',
        'thumb': _select_logo(channel.get('images', []), ''),
        'category': category_lookup.get(channel_id, '') or _extract_category(channel),
        'stitched_url': '',
        'slug': channel.get('slug', '') or '',
        'channel_id': channel_id,
        'country_code': normalize_country_code(country),
        'country_label': country_label(country),
    }


def _fetch_channels_service(country='auto'):
    country = normalize_country_code(country)
    cache_key = ('service_channels', country)
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    stale = _cache_get_stale(cache_key) or []
    params = {'channelIds': '', 'offset': '0', 'limit': '1000', 'sort': 'number:asc'}
    channels = []
    categories = {}
    try:
        response = SESSION.get(SERVICE_CHANNELS_URL, params=params, headers=_auth_headers(country), timeout=12)
        response.raise_for_status()
        raw_channels = (response.json() or {}).get('data', []) or []
    except Exception as exc:
        _log_warning('Service channels falhou para o país {}: {}'.format(country, exc))
        raw_channels = []
    if raw_channels:
        try:
            response = SESSION.get(SERVICE_CATEGORIES_URL, params=params, headers=_auth_headers(country), timeout=12)
            response.raise_for_status()
            cat_data = (response.json() or {}).get('data', []) or []
            for item in cat_data:
                cat_name = item.get('name', '') or 'Geral'
                for channel_id in item.get('channelIDs', []) or []:
                    categories[channel_id] = cat_name
        except Exception as exc:
            _log_warning('Service categories falhou para o país {}: {}'.format(country, exc))
            _health_update(country, 'service_categories', False, safe_text(exc))
        for channel in raw_channels:
            normalized = _normalize_channel_service(channel, categories, country=country)
            if normalized:
                channels.append(normalized)
    if channels:
        channels.sort(key=lambda item: item.get('number', 0))
        _health_update(country, 'live_channels', True, 'service channels')
        return _cache_set(cache_key, channels, ttl=PLUTO_CHANNELS_TTL_SECONDS)
    if stale:
        _health_update(country, 'live_channels', True, 'usando último payload válido de service channels', extra={'stale': True})
        return list(stale)
    _health_update(country, 'live_channels', False, 'service channels vazio')
    return _cache_set(cache_key, [], ttl=PLUTO_CHANNELS_TTL_SECONDS)


def _fetch_channels_legacy(country='auto', include_epg=False):
    country = normalize_country_code(country)
    cache_key = ('legacy_channels', country, bool(include_epg))
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    stale = _cache_get_stale(cache_key) or []
    now_utc, stop_utc = _window_for_fetch(include_epg)
    params = {
        'start': now_utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
        'stop': stop_utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
        'sid': get_session_id() or new_session_id(),
        'deviceId': uuid.uuid4().hex,
    }
    normalized = []
    last_exc = None
    for candidate in _build_geo_candidates(country, include_direct=True):
        try:
            response = SESSION.get(LEGACY_GUIDE_URL if include_epg else LEGACY_CHANNELS_URL, params=params, headers=_legacy_headers(country, candidate), timeout=15)
            response.raise_for_status()
            channels = response.json()
            if not isinstance(channels, list):
                channels = []
            for channel in channels:
                item = _normalize_channel_legacy(channel, country=country)
                if item:
                    if include_epg:
                        item['timelines'] = channel.get('timelines', []) or []
                    normalized.append(item)
            if normalized:
                normalized.sort(key=lambda item: item.get('number', 0))
                _health_update(country, 'legacy_channels_epg' if include_epg else 'legacy_channels', True, _geo_candidate_text(candidate))
                return _cache_set(cache_key, normalized, ttl=PLUTO_EPG_TTL_SECONDS if include_epg else PLUTO_CHANNELS_TTL_SECONDS)
        except Exception as exc:
            last_exc = exc
            _log_warning('Legacy channels falhou para {} (include_epg={}): {}'.format(_geo_candidate_text(candidate), include_epg, exc))
    if stale:
        _health_update(country, 'legacy_channels_epg' if include_epg else 'legacy_channels', True, 'usando último payload válido legacy', extra={'stale': True})
        return list(stale)
    _health_update(country, 'legacy_channels_epg' if include_epg else 'legacy_channels', False, safe_text(last_exc or 'legacy vazio'))
    return _cache_set(cache_key, [], ttl=PLUTO_EPG_TTL_SECONDS if include_epg else PLUTO_CHANNELS_TTL_SECONDS)


def _fetch_channels_single(country='auto'):
    channels = _fetch_channels_service(country)
    if channels:
        return channels
    _log_warning('Service channels vazio para o país {}. Tentando fallback legacy.'.format(country))
    channels = _fetch_channels_legacy(country, include_epg=False)
    if channels:
        _health_update(country, 'live_listing', True, 'fallback legacy aplicado')
        return channels
    _health_update(country, 'live_listing', False, 'service e legacy sem dados')
    return []


def _extract_program_metadata_from_timelines(timelines, channel_name, now_utc, country='auto', timezone_mode='auto'):
    current_program = None
    next_program = None
    for idx, timeline in enumerate(timelines or []):
        start = _parse_iso_datetime(timeline.get('start'))
        stop = _parse_iso_datetime(timeline.get('stop'))
        if not start or not stop:
            continue
        if start <= now_utc <= stop:
            ep = timeline.get('episode', {}) or {}
            current_program = {
                'title': ep.get('name', '') or timeline.get('title', ''),
                'description': ep.get('description', '') or timeline.get('description', ''),
                'start': start,
            }
            if idx + 1 < len(timelines):
                next_timeline = timelines[idx + 1]
                next_start = _parse_iso_datetime(next_timeline.get('start'))
                next_ep = next_timeline.get('episode', {}) or {}
                next_program = {
                    'title': next_ep.get('name', '') or next_timeline.get('title', ''),
                    'description': next_ep.get('description', '') or next_timeline.get('description', ''),
                    'start': next_start,
                }
            break
    tzinfo = _epg_timezone(country, timezone_mode=timezone_mode)
    description = ''
    display_name = channel_name or 'Pluto TV'
    if current_program and current_program.get('title'):
        local_now = current_program['start'].astimezone(tzinfo)
        description += '[COLOR yellow][{}] {}[/COLOR]\n({})\n'.format(
            local_now.strftime('%H:%M'), current_program.get('title', ''), current_program.get('description', '')
        )
        display_name = '{} - [COLOR yellow]{}[/COLOR]'.format(display_name, current_program.get('title', ''))
    if next_program and next_program.get('title') and next_program.get('start'):
        local_next = next_program['start'].astimezone(tzinfo)
        description += '[COLOR yellow][{}] {}[/COLOR]\n({})\n'.format(
            local_next.strftime('%H:%M'), next_program.get('title', ''), next_program.get('description', '')
        )
    return display_name, description


def _fetch_epg_overlay_single(country='auto', timezone_mode='auto'):
    country = normalize_country_code(country)
    cache_key = ('epg_overlay', country, str(timezone_mode or 'auto'))
    cached = _cache_get(cache_key)
    if cached is not None:
        return dict(cached)
    overlay = {}
    now_utc = datetime.now(timezone.utc)
    channels = _fetch_channels_legacy(country, include_epg=True)
    if not channels:
        channels = _fetch_service_timelines_overlay(country=country, timezone_mode=timezone_mode)
        return _cache_set(cache_key, channels, ttl=PLUTO_EPG_TTL_SECONDS)
    for channel in channels:
        display_name, description = _extract_program_metadata_from_timelines(
            channel.get('timelines', []), channel.get('name', ''), now_utc, country=country, timezone_mode=timezone_mode
        )
        overlay[_channel_key(channel)] = {
            'display_name': display_name or channel.get('name', ''),
            'description': description or ''
        }
    return _cache_set(cache_key, overlay, ttl=PLUTO_EPG_TTL_SECONDS)


def _fetch_service_timelines_overlay(country='auto', timezone_mode='auto'):
    country = normalize_country_code(country)
    cache_key = ('service_timelines_overlay', country, str(timezone_mode or 'auto'))
    cached = _cache_get(cache_key)
    if cached is not None:
        return dict(cached)
    stale = _cache_get_stale(cache_key) or {}
    channels = _fetch_channels_service(country)
    if not channels:
        return stale or {}
    headers = _auth_headers(country)
    channel_ids = [item.get('channel_id', '') for item in channels if item.get('channel_id')]
    channel_lookup = {item.get('channel_id', ''): item for item in channels if item.get('channel_id')}
    now_utc = datetime.now(timezone.utc)
    from_str = now_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    overlay = {}
    group_size = 100
    for idx in range(0, len(channel_ids), group_size):
        group = channel_ids[idx:idx + group_size]
        params = {'start': from_str, 'channelIds': ','.join(group), 'duration': '1440'}
        try:
            response = SESSION.get(SERVICE_TIMELINES_URL, params=params, headers=headers, timeout=12)
            response.raise_for_status()
            data = (response.json() or {}).get('data', []) or []
        except Exception as exc:
            _log_warning('Service timelines falhou para o país {}: {}'.format(country, exc))
            _health_update(country, 'epg_overlay', False, safe_text(exc))
            if stale:
                return stale
            data = []
        for entry in data:
            channel_id = entry.get('channelId', '')
            item = channel_lookup.get(channel_id, {})
            display_name, description = _extract_program_metadata_from_timelines(
                entry.get('timelines', []), item.get('name', ''), now_utc, country=country, timezone_mode=timezone_mode
            )
            overlay[_channel_key(item)] = {
                'display_name': display_name or item.get('name', ''),
                'description': description or ''
            }
    if overlay:
        _health_update(country, 'epg_overlay', True, 'service timelines')
        return _cache_set(cache_key, overlay, ttl=PLUTO_EPG_TTL_SECONDS)
    return stale or {}


def _apply_epg_overlay(channels, include_epg=True, timezone_mode='auto'):
    base = [dict(item) for item in (channels or [])]
    if not include_epg:
        for item in base:
            item['display_name'] = item.get('name') or '#{}'.format(item.get('number', ''))
            item['description'] = ''
        return base
    grouped_countries = {}
    for item in base:
        grouped_countries.setdefault(item.get('country_code', 'auto'), []).append(item)
    overlay_by_country = {}
    for country in grouped_countries.keys():
        try:
            overlay_by_country[country] = _fetch_epg_overlay_single(country=country, timezone_mode=timezone_mode)
        except Exception:
            overlay_by_country[country] = {}
    for item in base:
        default_name = item.get('name') or '#{}'.format(item.get('number', ''))
        overlay = overlay_by_country.get(item.get('country_code', 'auto'), {})
        meta = overlay.get(_channel_key(item), {})
        item['display_name'] = meta.get('display_name') or default_name
        item['description'] = meta.get('description') or ''
    return base


def _preferred_country_order(selected_country='auto'):
    selected_country = normalize_country_code(selected_country)
    order = []
    if selected_country != 'auto':
        order.append(selected_country)
    for code in COUNTRY_ORDER:
        if code not in order:
            order.append(code)
    if 'auto' not in order:
        order.append('auto')
    return order


def _dedupe_key_for_item(item):
    for key in ('slug', 'channel_id', 'id', 'series_id'):
        value = str(item.get(key, '') or '').strip().lower()
        if value:
            return key + ':' + value
    name = str(item.get('name', '') or item.get('display_name', '') or '').strip().lower()
    if name:
        return 'name:' + name
    return repr(sorted(item.items()))


def _dedupe_items(items, merge_duplicates=True, preferred_country='auto'):
    if not merge_duplicates:
        return list(items)
    order = _preferred_country_order(preferred_country)
    priority = {code: idx for idx, code in enumerate(order)}
    deduped = {}
    for item in items:
        key = _dedupe_key_for_item(item)
        current = deduped.get(key)
        if current is None:
            deduped[key] = item
            continue
        current_score = priority.get(current.get('country_code', 'auto'), 999)
        new_score = priority.get(item.get('country_code', 'auto'), 999)
        if new_score < current_score:
            deduped[key] = item
    return list(deduped.values())


def get_channels(hide_no_logo=False, include_epg=True, country='auto', all_countries=False,
                 merge_duplicates=True, prioritize_country=True, timezone_mode='auto'):
    selected_country = normalize_country_code(country)
    cache_key = ('channels', bool(hide_no_logo), bool(include_epg), selected_country, bool(all_countries), bool(merge_duplicates), bool(prioritize_country), str(timezone_mode or 'auto'))
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    countries = expand_country_codes(selected_country, all_countries=all_countries)
    channels = []
    for country_code in countries:
        rows = _fetch_channels_single(country=country_code)
        for item in rows:
            row = dict(item)
            if hide_no_logo and not row.get('thumb'):
                continue
            if all_countries and not merge_duplicates:
                row['name'] = '{}{}'.format(row.get('name', 'Pluto TV'), _format_country_suffix(country_code, True))
            channels.append(row)
    preferred = selected_country if prioritize_country else 'auto'
    channels = _dedupe_items(channels, merge_duplicates=merge_duplicates, preferred_country=preferred)
    channels.sort(key=lambda item: (item.get('number', 0), item.get('name', '').lower()))
    channels = _apply_epg_overlay(channels, include_epg=include_epg, timezone_mode=timezone_mode)
    return _cache_set(cache_key, channels, ttl=PLUTO_EPG_TTL_SECONDS if include_epg else PLUTO_CHANNELS_TTL_SECONDS)


def get_channels_grouped_by_category(hide_no_logo=False, include_epg=True, country='auto', all_countries=False,
                                     merge_duplicates=True, prioritize_country=True, timezone_mode='auto'):
    channels = get_channels(hide_no_logo=hide_no_logo, include_epg=include_epg, country=country,
                            all_countries=all_countries, merge_duplicates=merge_duplicates,
                            prioritize_country=prioritize_country, timezone_mode=timezone_mode)
    grouped = {}
    for channel in channels:
        category = channel.get('category') or 'Geral'
        grouped.setdefault(category, []).append(channel)
    return sorted(grouped.items(), key=lambda item: item[0].lower())


def get_channels_for_category(category, hide_no_logo=False, include_epg=True, country='auto', all_countries=False,
                              merge_duplicates=True, prioritize_country=True, timezone_mode='auto'):
    wanted = (category or '').strip().lower()
    return [
        item for item in get_channels(hide_no_logo=hide_no_logo, include_epg=include_epg, country=country,
                                      all_countries=all_countries, merge_duplicates=merge_duplicates,
                                      prioritize_country=prioritize_country, timezone_mode=timezone_mode)
        if (item.get('category') or 'Geral').strip().lower() == wanted
    ]


def _build_live_stream_url(channel_id, country='auto'):
    boot_data = _fetch_boot_data(country)
    token = boot_data.get('sessionToken', '') or ''
    stitcher_url = boot_data.get('stitcherUrl', STITCHER_FALLBACK) or STITCHER_FALLBACK
    stitcher_params = (boot_data.get('stitcherParams', '') or '').lstrip('?&')
    if not channel_id:
        return None
    stream_url = '{}/v2/stitch/hls/channel/{}/master.m3u8?jwt={}&masterJWTPassthrough=true'.format(
        stitcher_url.rstrip('/'), channel_id, token
    )
    if stitcher_params:
        stream_url += '&' + stitcher_params
    return stream_url + '|User-Agent=' + quote_plus(USER_AGENT)


def _build_stream_url_from_stitched(stitched_url, country='auto'):
    if not stitched_url:
        return None
    boot_data = _fetch_boot_data(country)
    token = boot_data.get('sessionToken', '') or ''
    stitcher_url = boot_data.get('stitcherUrl', STITCHER_FALLBACK) or STITCHER_FALLBACK
    stitcher_params = (boot_data.get('stitcherParams', '') or '').lstrip('?&')
    path = stitched_url.split('?')[0]
    path = re.sub(r'^https?://[^/]+', '', path)
    if path.startswith('/stitch/'):
        path = '/v2' + path
    if not path.startswith('/'):
        path = '/' + path
    stream_url = '{}{}?jwt={}&masterJWTPassthrough=true'.format(stitcher_url.rstrip('/'), path, token)
    if stitcher_params:
        stream_url += '&' + stitcher_params
    return stream_url + '|User-Agent=' + quote_plus(USER_AGENT)


def resolve_stream_url(slug='', channel_id='', stitched_url='', country='auto'):
    country = normalize_country_code(country)
    session_id = get_session_id() or ensure_session('')
    cache_key = ('stream', session_id, country, slug or '', channel_id or '', stitched_url or '')
    cached = _cache_get(cache_key)
    if cached is not None:
        return cached
    if channel_id:
        stream_url = _build_live_stream_url(channel_id=channel_id, country=country)
        if stream_url:
            return _cache_set(cache_key, stream_url, ttl=PLUTO_BOOT_TTL_SECONDS)
    stream_url = _build_stream_url_from_stitched(stitched_url, country=country)
    if not stream_url:
        return None
    return _cache_set(cache_key, stream_url, ttl=PLUTO_BOOT_TTL_SECONDS)


def _get_vod_categories_single(country='auto'):
    country = normalize_country_code(country)
    cache_key = ('vod_categories', country)
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    stale = _cache_get_stale(cache_key) or []
    try:
        response = SESSION.get(VOD_CATEGORIES_URL, headers=_auth_headers(country), timeout=15)
        response.raise_for_status()
        data = response.json()
        if isinstance(data, dict):
            categories = data.get('categories', []) or data.get('data', []) or []
        elif isinstance(data, list):
            categories = data
        else:
            categories = []
        if categories:
            _health_update(country, 'vod_categories', True, 'categorias VOD')
            return _cache_set(cache_key, categories, ttl=PLUTO_VOD_TTL_SECONDS)
    except Exception as exc:
        _log_warning('VOD categories falhou para o país {}: {}'.format(country, exc))
        _health_update(country, 'vod_categories', False, safe_text(exc))
    return list(stale)


def _pick_cover(covers, index=0):
    if not isinstance(covers, list) or not covers:
        return ''
    if index < len(covers) and isinstance(covers[index], dict):
        return covers[index].get('url', '') or ''
    for item in covers:
        if isinstance(item, dict) and item.get('url'):
            return item.get('url')
    return ''


def _normalize_vod_item(item, country='auto'):
    item_id = item.get('_id', '') or item.get('id', '') or ''
    if not item_id:
        return None
    media_type = item.get('type', '') or ''
    stitched_urls = []
    if media_type == 'movie':
        stitched_urls = item.get('stitched', {}).get('urls', []) or []
        if not stitched_urls:
            return None
    covers = item.get('covers', []) or []
    rating = item.get('rating', '') or ''
    return {
        'id': item_id,
        'name': item.get('name', '') or 'Pluto',
        'summary': item.get('summary', '') or item.get('description', '') or '',
        'description': item.get('description', '') or item.get('summary', '') or '',
        'genre': item.get('genre', '') or '',
        'rating': rating,
        'duration': int(item.get('duration', '0') or 0) // 1000,
        'poster': _pick_cover(covers, 0),
        'fanart': _pick_cover(covers, 2) or _pick_cover(covers, 1),
        'type': media_type,
        'stitched_url': stitched_urls[0].get('url', '') if stitched_urls else '',
        'seasons_numbers': item.get('seasonsNumbers', []) or [],
        'country_code': normalize_country_code(country),
        'country_label': country_label(country),
        'slug': item.get('slug', '') or '',
    }


def _collect_vod_categories(media_type='movie', country='auto', all_countries=False,
                            merge_duplicates=True, prioritize_country=True):
    selected_country = normalize_country_code(country)
    categories = []
    countries = expand_country_codes(selected_country, all_countries=all_countries)
    for country_code in countries:
        raw_categories = _get_vod_categories_single(country_code)
        for raw_category in raw_categories:
            category_id = raw_category.get('_id', '') or raw_category.get('id', '') or ''
            category_name = raw_category.get('name', '') or 'Pluto'
            items = raw_category.get('items', []) or []
            count = 0
            for item in items:
                if (item.get('type', '') or '') != media_type:
                    continue
                if _normalize_vod_item(item, country=country_code):
                    count += 1
            if count <= 0:
                continue
            categories.append({
                'key': '{}:{}'.format(country_code, category_id),
                'category_id': category_id,
                'name': category_name,
                'item_count': count,
                'country_code': country_code,
                'country_label': country_label(country_code),
            })
    if not all_countries or not merge_duplicates:
        categories.sort(key=lambda item: (item.get('name', '').lower(), item.get('country_label', '').lower()))
        return categories
    priority = {code: idx for idx, code in enumerate(_preferred_country_order(selected_country if prioritize_country else 'auto'))}
    merged = {}
    for item in categories:
        name_key = item.get('name', '').strip().lower()
        current = merged.get(name_key)
        if current is None:
            item['source_keys'] = [item['key']]
            merged[name_key] = item
            continue
        current['item_count'] += item.get('item_count', 0)
        current.setdefault('source_keys', []).append(item['key'])
        current_score = priority.get(current.get('country_code', 'auto'), 999)
        new_score = priority.get(item.get('country_code', 'auto'), 999)
        if new_score < current_score:
            item['item_count'] = current['item_count']
            item['source_keys'] = current.get('source_keys', [])
            merged[name_key] = item
    result = list(merged.values())
    result.sort(key=lambda item: item.get('name', '').lower())
    return result


def get_vod_categories(media_type='movie', country='auto', all_countries=False,
                       merge_duplicates=True, prioritize_country=True):
    cache_key = ('vod_categories_filtered', media_type, normalize_country_code(country), bool(all_countries), bool(merge_duplicates), bool(prioritize_country))
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    result = _collect_vod_categories(media_type=media_type, country=country, all_countries=all_countries,
                                     merge_duplicates=merge_duplicates, prioritize_country=prioritize_country)
    return _cache_set(cache_key, result, ttl=PLUTO_VOD_TTL_SECONDS)


def _find_vod_items_for_source_key(source_key, media_type='movie'):
    try:
        country_code, category_id = source_key.split(':', 1)
    except Exception:
        return []
    raw_categories = _get_vod_categories_single(country_code)
    for category in raw_categories:
        current_id = category.get('_id', '') or category.get('id', '') or ''
        if current_id != category_id:
            continue
        items = []
        for raw_item in category.get('items', []) or []:
            if (raw_item.get('type', '') or '') != media_type:
                continue
            normalized = _normalize_vod_item(raw_item, country=country_code)
            if normalized:
                items.append(normalized)
        return items
    return []


def get_vod_items(category_key, media_type='movie', country='auto', all_countries=False,
                  merge_duplicates=True, prioritize_country=True):
    cache_key = ('vod_items', category_key, media_type, normalize_country_code(country), bool(all_countries), bool(merge_duplicates), bool(prioritize_country))
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    categories = get_vod_categories(media_type=media_type, country=country, all_countries=all_countries,
                                    merge_duplicates=merge_duplicates, prioritize_country=prioritize_country)
    category = None
    for item in categories:
        if item.get('key') == category_key:
            category = item
            break
    if category is None:
        return []
    source_keys = category.get('source_keys') or [category.get('key')]
    items = []
    for source_key in source_keys:
        items.extend(_find_vod_items_for_source_key(source_key, media_type=media_type))
    if all_countries and not merge_duplicates:
        for item in items:
            item['name'] = '{}{}'.format(item.get('name', 'Pluto'), _format_country_suffix(item.get('country_code', 'auto'), True))
    preferred = normalize_country_code(country) if prioritize_country else 'auto'
    items = _dedupe_items(items, merge_duplicates=merge_duplicates, preferred_country=preferred)
    items.sort(key=lambda row: row.get('name', '').lower())
    return _cache_set(cache_key, items, ttl=PLUTO_VOD_TTL_SECONDS)


def get_series_seasons(series_id, country='auto'):
    country = normalize_country_code(country)
    cache_key = ('vod_series', series_id, country)
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    stale = _cache_get_stale(cache_key) or []
    try:
        url = VOD_SERIES_URL.format(series_id=series_id)
        response = SESSION.get(url, headers=_auth_headers(country), timeout=15)
        response.raise_for_status()
        data = response.json() or {}
        seasons = data.get('seasons', []) or []
    except Exception as exc:
        _log_warning('VOD seasons falhou para series_id={} país {}: {}'.format(series_id, country, exc))
        _health_update(country, 'vod_series', False, safe_text(exc), extra={'series_id': series_id})
        return list(stale)
    normalized = []
    for season in seasons:
        season_number = season.get('number', 0)
        try:
            season_number = int(season_number)
        except Exception:
            season_number = 0
        episodes = []
        for episode in season.get('episodes', []) or []:
            episode_id = episode.get('_id', '') or episode.get('id', '') or ''
            stitched_urls = episode.get('stitched', {}).get('urls', []) or []
            if not episode_id or not stitched_urls:
                continue
            covers = episode.get('covers', []) or []
            episodes.append({
                'id': episode_id,
                'name': episode.get('name', '') or 'Episódio',
                'description': episode.get('description', '') or episode.get('summary', '') or '',
                'summary': episode.get('summary', '') or episode.get('description', '') or '',
                'episode_number': int(episode.get('number', 0) or 0),
                'season_number': season_number,
                'duration': int(episode.get('duration', '0') or 0) // 1000,
                'poster': _pick_cover(covers, 0),
                'fanart': _pick_cover(covers, 2) or _pick_cover(covers, 1),
                'stitched_url': stitched_urls[0].get('url', '') or '',
                'country_code': country,
                'country_label': country_label(country),
            })
        episodes.sort(key=lambda row: row.get('episode_number', 0))
        normalized.append({
            'season_number': season_number,
            'name': season.get('name', '') or 'Temporada {}'.format(season_number),
            'description': season.get('description', '') or '',
            'episode_count': len(episodes),
            'episodes': episodes,
            'country_code': country,
            'country_label': country_label(country),
        })
    normalized.sort(key=lambda row: row.get('season_number', 0))
    if normalized:
        _health_update(country, 'vod_series', True, 'temporadas/episódios carregados', extra={'series_id': series_id})
        return _cache_set(cache_key, normalized, ttl=PLUTO_VOD_TTL_SECONDS)
    return list(stale)


def resolve_vod_stream_url(stitched_url='', country='auto'):
    country = normalize_country_code(country)
    session_id = get_session_id() or ensure_session('')
    cache_key = ('vod_stream', session_id, country, stitched_url or '')
    cached = _cache_get(cache_key)
    if cached is not None:
        return cached
    if not stitched_url:
        return None
    stream_url = _build_stream_url_from_stitched(stitched_url, country=country)
    if not stream_url:
        return None
    return _cache_set(cache_key, stream_url, ttl=PLUTO_BOOT_TTL_SECONDS)


def probe_health(country='auto'):
    country = normalize_country_code(country)
    snapshot = get_health_snapshot(country)
    if snapshot:
        return snapshot
    try:
        _fetch_boot_data(country)
    except Exception:
        pass
    try:
        _fetch_channels_service(country)
    except Exception:
        pass
    try:
        _get_vod_categories_single(country)
    except Exception:
        pass
    return get_health_snapshot(country)


def playlist_pluto(country='auto', all_countries=False, merge_duplicates=True, prioritize_country=True, timezone_mode='auto'):
    return [
        (
            item.get('display_name') or item.get('name') or 'Pluto TV',
            item.get('description', ''),
            item.get('thumb', ''),
            resolve_stream_url(item.get('slug', ''), item.get('channel_id', ''), item.get('stitched_url', ''), country=item.get('country_code', country))
        )
        for item in get_channels(
            hide_no_logo=False,
            include_epg=True,
            country=country,
            all_countries=all_countries,
            merge_duplicates=merge_duplicates,
            prioritize_country=prioritize_country,
            timezone_mode=timezone_mode
        )
    ]
