import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import urllib.request
import urllib.parse
import gzip
import io
import re
import json
import xml.etree.ElementTree as ET
import os
import time
from datetime import datetime, timedelta, timezone

from endpoints import M3U8_URL, XMLTV_URL, SPORTS_SCHEDULE_M3U8, SPORTS_SCHEDULE_XMLTV
from tools import debug_log
from cache import load_cache, save_cache, _safe_progress_update, get_cache_files

ADDON = xbmcaddon.Addon()

# In-memory cache state (managed internally by this module)
_M3U8_CACHE = None
_M3U8_CACHE_TIME = None
_XMLTV_CACHE = None
_XMLTV_CACHE_TIME = None

def get_url_settings(guide_number=1):
    m3u8_url = ADDON.getSetting('m3u8_url') if guide_number == 1 else ADDON.getSetting('m3u8_url_2')
    xmltv_url = ADDON.getSetting('xmltv_url') if guide_number == 1 else ADDON.getSetting('xmltv_url_2')
    if not m3u8_url:
        m3u8_url = M3U8_URL if guide_number == 1 else SPORTS_SCHEDULE_M3U8
    if not xmltv_url:
        xmltv_url = XMLTV_URL if guide_number == 1 else SPORTS_SCHEDULE_XMLTV
    prefix = 'epg' if guide_number == 1 else 'epg2'
    extra_sources = []
    for i in range(1, 11):
        extra_url = ADDON.getSetting(f'{prefix}_extra_{i}_url') or ''
        extra_offset = float(ADDON.getSetting(f'{prefix}_extra_{i}_offset') or 0)
        if extra_url.strip():
            extra_sources.append((extra_url.strip(), extra_offset))
    return m3u8_url, xmltv_url, extra_sources

def get_active_guide():
    window = xbmcgui.Window(10000)
    return int(window.getProperty('tvguide.active_guide') or '1')

def set_active_guide(guide_number):
    window = xbmcgui.Window(10000)
    window.setProperty('tvguide.active_guide', str(guide_number))

def normalize_id(text):
    if not text:
        return ""
    text = text.lower()
    text = text.replace('.us', '').replace(',', '').replace('"', '').strip()
    text = re.sub(r'\s+', '.', text)
    text = re.sub(r'\.+', '.', text)
    return text

def _parse_suffix_offsets_setting(setting_value):
    offsets = {}
    if not setting_value:
        return offsets
    parts = re.split(r'[;,\n]+', setting_value)
    for part in parts:
        part = (part or '').strip()
        if not part or '=' not in part:
            continue
        suffix, value = part.split('=', 1)
        suffix = re.sub(r'[^a-z0-9]', '', (suffix or '').strip().lower())
        if not suffix:
            continue
        try:
            offsets[suffix] = float((value or '').strip())
        except Exception:
            continue
    return offsets

def _get_channel_suffix_offset_hours(channel_id, suffix_offsets):
    if not channel_id or not suffix_offsets:
        return 0.0
    normalized = str(channel_id).strip().lower()
    normalized = re.sub(r'[^a-z0-9._-]', '', normalized)
    match = re.search(r'([a-z0-9]+)$', normalized)
    if not match:
        return 0.0
    suffix = match.group(1)
    return float(suffix_offsets.get(suffix, 0.0))

def parse_m3u8(path, progress_cb=None):
    global _M3U8_CACHE, _M3U8_CACHE_TIME
    current_time = datetime.now()
    cache_files = get_cache_files(get_active_guide())
    m3u8_cache_file = cache_files['m3u8']
    m3u8_cache_time_file = cache_files['m3u8_time']

    cached_data, cached_time = load_cache(m3u8_cache_file, m3u8_cache_time_file)
    if cached_data:
        _M3U8_CACHE = cached_data
        _M3U8_CACHE_TIME = cached_time
        _safe_progress_update(progress_cb, 30, f"Using cached channel data ({len(cached_data)} channels)...")
        return cached_data

    channels = []
    channel_number = 1
    try:
        _safe_progress_update(progress_cb, 5, 'Fetching M3U8 playlist...')
        headers = {'User-Agent': 'Mozilla/5.0'}
        request = urllib.request.Request(path, headers=headers)
        with urllib.request.urlopen(request, timeout=10) as response:
            content = response.read().decode('utf-8', errors='ignore')
        lines = content.splitlines()
        current_channel = None
        total_lines = max(1, len(lines))
        _safe_progress_update(progress_cb, 12, f'Parsing M3U8 lines ({total_lines})...')

        for idx, line in enumerate(lines):
            if idx % 500 == 0:
                pct = 12 + int((idx / float(total_lines)) * 18)
                _safe_progress_update(progress_cb, min(30, pct), f'Parsing M3U8... {idx}/{total_lines} lines')
            line = line.strip()
            if line.startswith('#EXTINF'):
                try:
                    tvg_id_match = re.search(r'tvg-id="([^"]*)"', line)
                    tvg_name_match = re.search(r'tvg-name="([^"]*)"', line)
                    logo_match = re.search(r'tvg-logo="([^"]*)"', line)
                    group_match = re.search(r'group-title="([^"]*)"', line)
                    title_match = re.search(r',(?=(?:(?:"[^"]*")|[^"])*$)\s*([^\n]+)$', line)

                    if title_match:
                        name = title_match.group(1).strip()
                    elif tvg_name_match and tvg_name_match.group(1).strip():
                        name = tvg_name_match.group(1).strip()
                    else:
                        name = f"Channel {channel_number}"

                    tvg_id = tvg_id_match.group(1) if tvg_id_match else normalize_id(name)
                    logo = logo_match.group(1) if logo_match else ""
                    group = group_match.group(1) if group_match else ""

                    current_channel = {
                        'name': name,
                        'tvg_id': tvg_id,
                        'logo': logo,
                        'group': group,
                        'normalized_name': normalize_id(name)
                    }
                except Exception:
                    current_channel = None
                    continue
            elif line and not line.startswith('#') and current_channel:
                current_channel['url'] = line
                channels.append(current_channel)
                channel_number += 1
                current_channel = None
    except Exception:
        return []

    _M3U8_CACHE = channels
    _M3U8_CACHE_TIME = current_time
    save_cache(channels, m3u8_cache_file, m3u8_cache_time_file)
    _safe_progress_update(progress_cb, 30, f'M3U8 complete: {len(channels)} channels loaded')
    return channels

def parse_xmltv_from_url(path, progress_cb=None, suffix_offsets=None, extra_offset=0):
    try:
        _safe_progress_update(progress_cb, 35, 'Fetching XMLTV guide data...')
        cache_path = xbmcvfs.translatePath(ADDON.getAddonInfo('profile'))
        temp_file = os.path.join(cache_path, f'temp_epg_{int(time.time())}.xml.gz' if path.lower().endswith('.gz') else f'temp_epg_{int(time.time())}.xml')

        try:
            headers = {'User-Agent': 'Mozilla/5.0'}
            request = urllib.request.Request(path, headers=headers)

            with urllib.request.urlopen(request, timeout=30) as response:
                total_size = int(response.headers.get('Content-Length', 0))
                downloaded = 0
                chunk_size = 8192

                with open(temp_file, 'wb') as f:
                    while True:
                        chunk = response.read(chunk_size)
                        if not chunk:
                            break
                        f.write(chunk)
                        downloaded += len(chunk)
                        if total_size > 0 and progress_cb:
                            pct = 35 + int((downloaded / total_size) * 10)
                            _safe_progress_update(progress_cb, min(pct, 45), f'Downloading EPG... {downloaded//1024}KB')

            _safe_progress_update(progress_cb, 48, 'Decompressing and parsing...')

            programs = []
            local_tz = datetime.now().astimezone().tzinfo
            current_time = datetime.now().astimezone(local_tz)

            try:
                if path.lower().endswith('.gz') or temp_file.endswith('.gz'):
                    file_handle = gzip.open(temp_file, 'rb')
                else:
                    file_handle = open(temp_file, 'rb')

                context = ET.iterparse(file_handle, events=('start', 'end'))
                root = None
                programme_count = 0

                for event, elem in context:
                    if event == 'start':
                        if root is None:
                            root = elem
                    elif event == 'end':
                        if elem.tag == 'programme':
                            try:
                                channel = elem.get('channel')
                                start_str = elem.get('start')[:14]
                                stop_str = elem.get('stop')[:14]

                                edt = timezone(timedelta(hours=-4))
                                start = datetime.strptime(start_str, '%Y%m%d%H%M%S').replace(tzinfo=edt)
                                stop = datetime.strptime(stop_str, '%Y%m%d%H%M%S').replace(tzinfo=edt)
                                start_local = start.astimezone(local_tz)
                                stop_local = stop.astimezone(local_tz)

                                global_offset = float(ADDON.getSetting('epg_global_offset') or 0)
                                if global_offset != 0:
                                    start_local = start_local + timedelta(hours=global_offset)
                                    stop_local = stop_local + timedelta(hours=global_offset)

                                if extra_offset != 0:
                                    start_local = start_local + timedelta(hours=extra_offset)
                                    stop_local = stop_local + timedelta(hours=extra_offset)

                                if suffix_offsets:
                                    offset_hours = _get_channel_suffix_offset_hours(channel, suffix_offsets)
                                    if offset_hours:
                                        start_local = start_local + timedelta(hours=offset_hours)
                                        stop_local = stop_local + timedelta(hours=offset_hours)

                                if stop_local < current_time - timedelta(hours=1):
                                    elem.clear()
                                    root.remove(elem) if root is not None else None
                                    continue

                                title_elem = elem.find('title')
                                title = title_elem.text if title_elem is not None else "No Title"

                                desc_elem = elem.find('desc')
                                desc = desc_elem.text if desc_elem is not None else ""

                                icon_elem = elem.find('icon')
                                icon = {'src': icon_elem.get('src')} if icon_elem is not None and icon_elem.get('src') else None

                                sub_title_elem = elem.find('sub-title')
                                sub_title = sub_title_elem.text if sub_title_elem is not None else ""

                                categories = [cat.text for cat in elem.findall('category') if cat.text]
                                episode_elem = elem.find('episode-num')
                                episode = episode_elem.text if episode_elem is not None else ""
                                rating_elem = elem.find('.//rating/value')
                                rating = rating_elem.text if rating_elem is not None else ""
                                actors = [actor.text for actor in elem.findall('.//credits/actor') if actor.text]

                                programs.append({
                                    'channel': channel,
                                    'start': start_local,
                                    'stop': stop_local,
                                    'title': title,
                                    'desc': desc,
                                    'sub-title': sub_title,
                                    'category': categories if categories else "",
                                    'episode-num': episode,
                                    'rating': rating,
                                    'actors': actors,
                                    'icon': icon
                                })

                                programme_count += 1
                                if programme_count % 500 == 0:
                                    _safe_progress_update(progress_cb, 50, f'Processing EPG... {programme_count} programs')
                            finally:
                                if root is not None:
                                    try:
                                        root.remove(elem)
                                    except:
                                        pass
                                elem.clear()

                        if progress_cb and programme_count > 0 and programme_count % 2000 == 0:
                            _safe_progress_update(progress_cb, 60, f'Processed {programme_count} programs...')

                _safe_progress_update(progress_cb, 90, f'XMLTV complete: {len(programs)} programs')
                return programs

            finally:
                file_handle.close()
                try:
                    os.remove(temp_file)
                except:
                    pass

        except Exception as e:
            debug_log(f"Failed to download/parse {path}: {str(e)}", xbmc.LOGERROR)
            import traceback
            debug_log(f"Traceback: {traceback.format_exc()}", xbmc.LOGERROR)
            return []

    except Exception as e:
        debug_log(f"parse_xmltv_from_url outer error: {str(e)}", xbmc.LOGERROR)
        return []

def parse_xmltv(path, progress_cb=None):
    global _XMLTV_CACHE, _XMLTV_CACHE_TIME
    current_time = datetime.now()

    suffix_offsets_setting = (ADDON.getSetting('epg_suffix_offsets') or '').strip()
    suffix_offsets = _parse_suffix_offsets_setting(suffix_offsets_setting)

    cache_files = get_cache_files(get_active_guide())
    xmltv_cache_file = cache_files['xmltv']
    xmltv_cache_time_file = cache_files['xmltv_time']

    cached_data, cached_time = load_cache(xmltv_cache_file, xmltv_cache_time_file)
    if cached_data:
        _XMLTV_CACHE = cached_data
        _XMLTV_CACHE_TIME = cached_time
        _safe_progress_update(progress_cb, 90, f"Using cached EPG data ({len(cached_data)} programs)...")
        return cached_data

    all_programs = []
    url_list = [path]
    offset_list = [0]

    _, _, extra_sources = get_url_settings(get_active_guide())
    if extra_sources:
        url_list.extend([url for url, _ in extra_sources])
        offset_list.extend([offset for _, offset in extra_sources])

    total_urls = len(url_list)
    for i, (url, extra_offset) in enumerate(zip(url_list, offset_list)):
        if total_urls > 1:
            _safe_progress_update(progress_cb, 35, f'Fetching EPG {i+1}/{total_urls}...')
        programs = parse_xmltv_from_url(url, progress_cb, suffix_offsets, extra_offset)
        all_programs.extend(programs)

    _XMLTV_CACHE = all_programs
    _XMLTV_CACHE_TIME = current_time
    save_cache(all_programs, xmltv_cache_file, xmltv_cache_time_file)
    _safe_progress_update(progress_cb, 90, f'XMLTV complete: {len(all_programs)} programs loaded')
    return all_programs
