import os
import re
import json
import gzip
import time
import requests
from io import BytesIO

EPG_SOURCE_URL = "https://iptv-epg.org/files/epg-cz.xml.gz"
CACHE_MAX_AGE = 21600  # 6 hours

SOURCE_IDS = {
    "JEDNOTKA": "10-stv1",
    "DVOJKA": "11-stv2",
    "STV_24": "1324-rtvs-24",
    "RTVSSPORT": "1512-rtvs-sport",
    "JOJ": "33-joj",
    "JOJ24": "15441-joj-24",
    "JOJPLUS": "206-joj-plus",
    "JOJSVET": "15509-joj-svet",
    "JOJCINEMA": "1023-joj-cinema",
    "JOJFAMILY": "1141-joj-family",
    "WAU": "718-wau",
    "JOJKO": "999-jojko",
    "TVRIK": "1408-rik-tv",
    "TUKI": "1021-tuki",
    "MARKIZA_KRIMI": "15402-markiza-krimi",
    "DOMA": "225-doma",
    "DAJTO": "683-tv-dajto",
    "TA3": "42-ta3",
    "TVLUX": "191-lux",
    "Folklorika": "1309-folklorika",
    "PRIMA": "4-prima",
    "PRIMAPLUS": "1162-prima-plus",
    "PRIMACOOL": "220-prima-cool",
    "PRIMAMAX": "1122-prima-max",
    "PRIMAKRIMI": "1196-prima-krimi",
    "PRIMALOVE": "496-prima-love",
    "PRIMAZOOM": "708-prima-zoom",
    "PRIMASTAR": "1513-prima-star",
    "PRIMASHOW": "15292-prima-show",
    "CNNPRIMANEWS": "1338-cnn-prima-news",
    "SEZNAMTV": "1192-seznam.cz-tv",
    "NOVA": "1-nova",
    "NOVAINT": "1125-nova-international",
    "NOVAFUN": "707-nova-fun",
    "NOVACINEMA": "132-nova-cinema",
    "NOVAACTION": "677-nova-action",
    "NOVAGOLD": "712-nova-gold",
    "NOVALADY": "15290-nova-lady",
    "NOVASPORT1": "205-nova-sport-1",
    "NOVASPORT2": "1038-nova-sport-2",
    "NOVASPORT3": "1520-nova-sport-3",
    "NOVASPORT4": "1521-nova-sport-4",
    "CSFILMMINI": "1149-cs-film",
    "CSHistory": "1187-cs-history",
    "CSMystery": "717-cs-mystery",
    "HORORFILM": "713-cs-horror",
    "FISHINGandHUNTINGUPC": "207-the-fishing-and-hunting",
    "DISCOVERYCHANNEL": "15-discovery",
    "DISCOVERYSCIENCE": "92-discovery-science",
    "HBO": "6-hbo",
    "HBO2": "54-hbo2",
    "HBO3": "115-hbo3",
    "CINEMAX": "66-cinemax",
    "CINEMAX2": "80-cinemax-2",
    "FILMBOX": "72-filmbox",
    "FILMBOXEXTRA": "182-filmbox-extra-hd",
    "FILMBOXPREMIUM": "592-filmbox-premium",
    "FILMBOXFAMILY": "576-filmbox-family",
    "FILMBOXSTARS": "76-filmbox-stars",
    "FILMBOXARTHOUSE": "982-filmbox-arthouse",
    "FIGHTBOX": "590-fightbox",
    "DOCUBOX": "733-docubox-hd",
    "EUROSPORT": "9-eurosport-1",
    "EUROSPORT2": "59-eurosport-2",
    "THEHISTORYCHANNEL": "202-history-channel",
    "CRIMEandINVESTIGATION": "832-crime-and-investigation",
    "ViasatExplore": "69-viasat-explorer",
    "VIASATHISTORY": "70-viasat-history",
    "VIASATNATURE": "249-viasat-nature",
    "ANIMALPLANET": "28-animalplanet",
    "NATIONALGEOGRAPHIC": "232-national-geographic-hd",
    "NATGEOWILD": "106-national-geographic-wild",
    "LoveNature": "1320-love-nature",
    "SPEKTRUM": "7-spektrum",
    "SPEKTRUMHOME": "77-spektrum-home",
    "TRAVELCHANNEL": "104-travel-channel",
    "TRAVELXP": "1199-travelxp-hd",
    "AXN": "55-axn",
    "AXNBLACK": "125-axn-black",
    "AXNWHITE": "126-axn-white",
    "VIASATEPICDRAMA": "1209-epic-drama",
    "CBSREALITY": "32-cbs-reality",
    "FILM+": "68-film+",
    "FILMEUROPE": "570-film-europe",
    "FILMEUROPEPLUS": "1341-film-europe-+",
    "CANALPLUSACTIONCZ": "15476-canal+-action",
    "IDXTRA": "223-investigation-discovery",
    "Skylink7": "723-skylink-7",
    "TLC": "549-tlc",
    "FOODNETWORK": "636-food-network",
    "UPNETWORK": "1169-up-network",
    "E!": "165-e!-entertainment",
    "HGTV": "15322-hgtv",
    "NAUTICALCHANNEL": "1176-nautical-channel",
    "ParamountNetwork": "1123-paramount-network",
    "BOOMERANG": "64-cartoonito",
    "CARTOONNETWORK": "168-cartoon-network",
    "DISNEYCHANNEL": "228-disney-channel",
    "DISNEYJUNIOR": "567-disney-junior",
    "NICKELODEON": "254-nickelodeon",
    "NICKTOON": "1314-nicktoons",
    "NICKJRTHORCZ": "572-nick-jr.",
    "MINIMAX": "57-minimax",
    "JIMJAM": "137-jim-jam",
    "HOBBYTV": "1213-hobby-tv",
    "TVPAPRIKA": "62-paprika",
    "AutoMotorSport": "704-auto-motor-sport",
    "TVNOE": "114-noe",
    "RETROMUSICTV": "248-retro-music",
    "TVBARRANDOV": "215-barrandov",
    "BARRANDOVKINO": "1018-kino-barrandov",
    "BARRANDOVKRIMI": "1020-barrandov-krimi",
    "GOLFCHANNEL": "127-golf-channel",
    "iCONCERTS": "543-iconcerts",
    "CanalplusSportCZ": "15412-canal+-sport",
    "CanalplusSport2": "mag-4560-canal+-sport-2",
    "CanalplusSport3": "mag-4561-canal+-sport-3",
    "CanalplusSport4": "mag-4562-canal+-sport-4",
    "CanalplusSport5": "mag-4563-canal+-sport-5",
    "CanalplusSport6": "mag-4564-canal+-sport-6",
    "ARENASPORT1": "687-arena-sport-1",
    "ARENASPORT2": "800-arena-sport-2",
    "SPORT1": "75-sport1",
    "SPORT2": "221-sport2",
    "SPORT5": "197-sport-5",
    "PREMIERSPORT1": "985-premier-sport-1",
    "PREMIERSPORT2CZ": "1522-premier-sport-2",
    "AMCCHANNEL": "82-amc",
}

NAME_MAP = {
    "JEDNOTKA": "10-stv1",
    "DVOJKA": "11-stv2",
    "STV_24": "1324-rtvs-24",
    "RTVS SPORT": "1512-rtvs-sport",
    "JOJ": "33-joj",
    "JOJ 24": "15441-joj-24",
    "JOJ PLUS": "206-joj-plus",
    "JOJ SVET": "15509-joj-svet",
    "JOJ CINEMA": "1023-joj-cinema",
    "JOJ FAMILY": "1141-joj-family",
    "WAU": "718-wau",
    "JOJKO": "999-jojko",
    "TV RIK": "1408-rik-tv",
    "DOMA": "225-doma",
    "DAJTO": "683-tv-dajto",
    "MARKIZA_KRIMI": "15402-markiza-krimi",
    "TA3": "42-ta3",
    "TV LUX": "191-lux",
    "Folklorika": "1309-folklorika",
    "PRIMA": "4-prima",
    "PRIMA PLUS": "1162-prima-plus",
    "PRIMA COOL": "220-prima-cool",
    "PRIMA MAX": "1122-prima-max",
    "PRIMA KRIMI": "1196-prima-krimi",
    "PRIMA LOVE": "496-prima-love",
    "PRIMA ZOOM": "708-prima-zoom",
    "PRIMA STAR": "1513-prima-star",
    "PRIMA SHOW": "15292-prima-show",
    "CNN PRIMA NEWS": "1338-cnn-prima-news",
    "SEZNAM TV": "1192-seznam.cz-tv",
    "NOVA": "1-nova",
    "NOVA INT.": "1125-nova-international",
    "NOVA FUN": "707-nova-fun",
    "NOVA CINEMA": "132-nova-cinema",
    "NOVA ACTION": "677-nova-action",
    "NOVA GOLD": "712-nova-gold",
    "NOVA LADY": "15290-nova-lady",
    "NOVA SPORT 1": "205-nova-sport-1",
    "NOVA SPORT 2": "1038-nova-sport-2",
    "NOVA SPORT 3": "1520-nova-sport-3",
    "NOVA SPORT 4": "1521-nova-sport-4",
    "CS FILM MINI": "1149-cs-film",
    "CS HISTORY": "1187-cs-history",
    "CS MYSTERY": "717-cs-mystery",
    "HOROR FILM": "713-cs-horror",
    "DISCOVERY CHANNEL": "15-discovery",
    "DISCOVERY SCIENCE": "92-discovery-science",
    "HBO": "6-hbo",
    "HBO 2": "54-hbo2",
    "HBO 3": "115-hbo3",
    "CINEMAX": "66-cinemax",
    "CINEMAX 2": "80-cinemax-2",
    "FILMBOX": "72-filmbox",
    "FILMBOX EXTRA": "182-filmbox-extra-hd",
    "FILMBOX PREMIUM": "592-filmbox-premium",
    "FILMBOX FAMILY": "576-filmbox-family",
    "FILMBOX STARS": "76-filmbox-stars",
    "FILMBOX ARTHOUSE": "982-filmbox-arthouse",
    "FIGHTBOX": "590-fightbox",
    "DOCUBOX": "733-docubox-hd",
    "EUROSPORT": "9-eurosport-1",
    "EUROSPORT 2": "59-eurosport-2",
    "THE HISTORY CHANNEL": "202-history-channel",
    "VIASAT EXPLORE": "69-viasat-explorer",
    "VIASAT HISTORY": "70-viasat-history",
    "VIASAT NATURE": "249-viasat-nature",
    "ANIMAL PLANET": "28-animalplanet",
    "NATIONAL GEOGRAPHIC": "232-national-geographic-hd",
    "NAT. GEO. WILD": "106-national-geographic-wild",
    "LOVE NATURE": "1320-love-nature",
    "SPEKTRUM": "7-spektrum",
    "SPEKTRUM HOME": "77-spektrum-home",
    "TRAVEL CHANNEL": "104-travel-channel",
    "TRAVEL XP": "1199-travelxp-hd",
    "AXN": "55-axn",
    "AXN BLACK": "125-axn-black",
    "AXN WHITE": "126-axn-white",
    "VIASAT EPIC DRAMA": "1209-epic-drama",
    "CBS REALITY": "32-cbs-reality",
    "FILM+": "68-film+",
    "FILM EUROPE": "570-film-europe",
    "FILM EUROPE PLUS": "1341-film-europe-+",
    "CANAL PLUS ACTION CZ": "15476-canal+-action",
    "ID XTRA": "223-investigation-discovery",
    "SKYLINK7": "723-skylink-7",
    "TLC": "549-tlc",
    "FOOD NETWORK": "636-food-network",
    "UP NETWORK": "1169-up-network",
    "E!": "165-e!-entertainment",
    "HGTV": "15322-hgtv",
    "NAUTICAL CHANNEL": "1176-nautical-channel",
    "PARAMOUNT NETWORK": "1123-paramount-network",
    "BOOMERANG": "64-cartoonito",
    "CARTOON NETWORK": "168-cartoon-network",
    "DISNEY CHANNEL": "228-disney-channel",
    "DISNEY JUNIOR": "567-disney-junior",
    "NICKELODEON": "254-nickelodeon",
    "NICKTOON": "1314-nicktoons",
    "NICK JR THOR CZ": "572-nick-jr.",
    "MINIMAX": "57-minimax",
    "JIMJAM": "137-jim-jam",
    "HOBBY TV": "1213-hobby-tv",
    "TV PAPRIKA": "62-paprika",
    "AUTO MOTOR SPORT": "704-auto-motor-sport",
    "TV NOE": "114-noe",
    "RETRO MUSIC TV": "248-retro-music",
    "TV BARRANDOV": "215-barrandov",
    "BARRANDOV KINO": "1018-kino-barrandov",
    "BARRANDOV KRIMI": "1020-barrandov-krimi",
    "NGC": "232-national-geographic-hd",
    "NGC WILD": "106-national-geographic-wild",
    "FOLKLORIKA TV": "1309-folklorika",
    "TELEVIZE SEZNAM": "1192-seznam.cz-tv",
    "LUX TV": "191-lux",
    "SPORT": "1512-rtvs-sport",
    "KRIMI BARRANDOV": "1020-barrandov-krimi",
    "CRIME AND INVESTIGATION": "832-crime-and-investigation",
    "TV DAJTO": "683-tv-dajto",
    "SEZNAM.CZ TV": "1192-seznam.cz-tv",
    "ŠLÁGR MUZIKA": "1198-slagr-muzika",
    "ŠLÁGR ORIGINÁL": "684-slagr-original",
    "ŤUKI": "1021-tuki",
    "E! ENTERTAINMENT": "165-e!-entertainment",
    "O2 SPORT": "1040-o2-sport",
    "RIK TV": "1408-rik-tv",
    "NOE": "114-noe",
    "BARRANDOV": "215-barrandov",
    "PAPRIKA": "62-paprika",
    "RETRO MUSIC": "248-retro-music",
    "GOLF CHANNEL": "127-golf-channel",
    "ICONCERTS": "543-iconcerts",
    "CANAL PLUS SPORT CZ": "15412-canal+-sport",
    "CANAL PLUS SPORT 2": "mag-4560-canal+-sport-2",
    "CANAL PLUS SPORT 3": "mag-4561-canal+-sport-3",
    "CANAL PLUS SPORT 4": "mag-4562-canal+-sport-4",
    "CANAL PLUS SPORT 5": "mag-4563-canal+-sport-5",
    "CANAL PLUS SPORT 6": "mag-4564-canal+-sport-6",
    "ARENA SPORT 1": "687-arena-sport-1",
    "ARENA SPORT 2": "800-arena-sport-2",
    "SPORT1": "75-sport1",
    "SPORT2": "221-sport2",
    "SPORT 5": "197-sport-5",
    "PREMIER SPORT 1": "985-premier-sport-1",
    "PREMIER SPORT 2 CZ": "1522-premier-sport-2",
    "AMC CHANNEL": "82-amc",

    "CT1": "2-ct1",
    "CT2": "3-ct2",
    "CT24": "65-ct24",
    "CT4": "73-ct4-sport",
    "CT5": "805-ct-:d",
    "CT6": "804-ct-art",
}


_DIACRITICS = str.maketrans({
    'Č': 'C', 'č': 'c', 'Ď': 'D', 'ď': 'd', 'Ě': 'E', 'ě': 'e',
    'Ň': 'N', 'ň': 'n', 'Ř': 'R', 'ř': 'r', 'Š': 'S', 'š': 's',
    'Ť': 'T', 'ť': 't', 'Ů': 'U', 'ů': 'u', 'Ž': 'Z', 'ž': 'z',
    'Á': 'A', 'á': 'a', 'É': 'E', 'é': 'e', 'Ĺ': 'L', 'ĺ': 'l',
    'Ľ': 'L', 'ľ': 'l', 'Í': 'I', 'í': 'i', 'Ó': 'O', 'ó': 'o',
    'Ú': 'U', 'ú': 'u', 'Ý': 'Y', 'ý': 'y', 'Ä': 'A', 'ä': 'a',
    'Ö': 'O', 'ö': 'o', 'Ü': 'U', 'ü': 'u', 'Ő': 'O', 'ő': 'o',
    'Ű': 'U', 'ű': 'u',
})

def _normalize(name):
    name = name.replace('&amp;', '&')
    name = name.translate(_DIACRITICS)
    name = name.lower().replace('+', 'plus')
    return re.sub(r'[^a-z0-9]', '', name)


def _strip_prefix(name):
    name = re.sub(r'^[A-Z]{2}\s*-\s*', '', name)
    return name.strip()


def _strip_suffixes(name):
    result = name
    while True:
        prev = result
        result = re.sub(r'\s*\.(sk|cz|hu|pl|ro|com|eu)\s*$', '', result, flags=re.IGNORECASE).strip()
        result = re.sub(r'\s*\b(hd|sk|cz|hu)\b\.?\s*$', '', result, flags=re.IGNORECASE).strip()
        result = re.sub(r'\s*\((pl|de|cz|hu|sk|uk)\)\s*$', '', result, flags=re.IGNORECASE).strip()
        if result == prev:
            break
    return result


def _build_iptv_index(raw_xml):
    index = {}
    for m in re.finditer(r'<channel\s+id="([^"]+)"[^>]*>\s*<display-name>([^<]+)</display-name>', raw_xml):
        iptv_id = m.group(1)
        display_name = _strip_prefix(m.group(2))
        candidates = {display_name}
        prev = None
        cur = display_name
        while cur != prev:
            candidates.add(cur)
            prev = cur
            cur = _strip_suffixes(cur)
        candidates.add(cur)
        for candidate in candidates:
            norm = _normalize(candidate)
            if norm and norm not in index:
                index[norm] = (iptv_id, display_name)
    return index


def _build_channel_map(raw_xml, channels_data):
    iptv_index = _build_iptv_index(raw_xml)
    channel_map = {}
    for display_name, our_epg in NAME_MAP.items():
        norm = _normalize(display_name)
        if norm in iptv_index:
            iptv_id, _ = iptv_index[norm]
            channel_map[iptv_id] = our_epg
    for ch_id, ch_data in channels_data.items():
        our_epg = ch_data.get("epg", "")
        if not our_epg or our_epg == "0":
            continue
        our_name = ch_data.get("name", "")
        norm = _normalize(our_name)
        if norm in iptv_index:
            iptv_id, display_name = iptv_index[norm]
            if iptv_id not in channel_map:
                channel_map[iptv_id] = our_epg
    return channel_map


def _escape_xml(text):
    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;").replace("'", "&apos;")


def _fix_mojibake(text):
    try:
        fixed = text.encode('latin-1').decode('utf-8')
        if 'Ä' in text and 'Č' in fixed:
            return fixed
    except:
        pass
    return text


def _is_valid_xmltv(text):
    stripped = text.lstrip()
    return stripped.startswith('<?xml') or stripped.startswith('<tv')


def _download(url, timeout=30):
    resp = requests.get(url, timeout=timeout)
    resp.raise_for_status()
    data = resp.content
    if url.endswith(".gz"):
        with gzip.GzipFile(fileobj=BytesIO(data)) as f:
            raw = f.read().decode("utf-8")
    else:
        raw = data.decode("utf-8")
    if '<span' in raw and 'download limit' in raw.lower():
        raise Exception("EPG source returned: download limit reached (try again tomorrow or use a different source)")
    return _fix_mojibake(raw)


EPG_SOURCES = {
    "default": "https://iptv-epg.org/files/epg-cz.xml.gz",
    "openepg": "https://www.open-epg.com/files/czech3.xml.gz",
}


def _download_or_cached(url, raw_path, max_age=CACHE_MAX_AGE, fallback=True):
    if os.path.exists(raw_path):
        age = time.time() - os.path.getmtime(raw_path)
        if age < max_age:
            with open(raw_path, "r", encoding="utf-8") as f:
                raw_xml = f.read()
            if _is_valid_xmltv(raw_xml):
                return raw_xml
    try:
        raw_xml = _download(url)
        if not _is_valid_xmltv(raw_xml):
            snippet = raw_xml.strip()[:120]
            raise Exception("EPG download returned invalid data (not XMLTV): %s" % snippet)
    except Exception:
        if fallback and os.path.exists(raw_path):
            with open(raw_path, "r", encoding="utf-8") as f:
                raw_xml = f.read()
            if _is_valid_xmltv(raw_xml):
                return raw_xml
        raise
    with open(raw_path, "w", encoding="utf-8") as f:
        f.write(raw_xml)
    return raw_xml


def _remap(raw_xml, channel_map):
    def _replace_id(m):
        prefix = m.group(1)
        old_id = m.group(2)
        new_id = channel_map.get(old_id, old_id)
        return '%s"%s"' % (prefix, new_id)

    remapped = re.sub(
        r'(<programme\s[^>]*channel=)"([^"]+)"',
        _replace_id,
        raw_xml
    )
    remapped = re.sub(
        r'(<channel\s+id=)"([^"]+)"',
        _replace_id,
        remapped
    )
    return remapped


def update_epg(userdata_folder, channels_data, source="default", source_url=""):
    raw_path = os.path.join(userdata_folder, "epg_raw.xml")
    output_path = os.path.join(userdata_folder, "epg.xml")

    if source == "default":
        raw_xml = _download_or_cached(EPG_SOURCES["default"], raw_path)
        channel_map = _build_channel_map(raw_xml, channels_data)
        remapped = _remap(raw_xml, channel_map)
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(remapped)

    elif source == "openepg":
        raw_xml = _download_or_cached(EPG_SOURCES["openepg"], raw_path)
        channel_map = _build_channel_map(raw_xml, channels_data)
        remapped = _remap(raw_xml, channel_map)
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(remapped)

    elif source == "custom":
        if not source_url:
            raise Exception("No custom EPG URL specified")
        raw_xml = _download_or_cached(source_url, raw_path, max_age=0, fallback=False)
        channel_map = _build_channel_map(raw_xml, channels_data)
        remapped = _remap(raw_xml, channel_map)
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(remapped)

    return output_path
