"""Debug info endpoint — lifted from web_server.py.

The function bodies are byte-identical to the originals. Module-level
shims for ``spotify_client`` and ``tidal_client`` (proxies that resolve
through the metadata registry / runtime client registry) plus injected
state dicts and helpers let the bodies resolve their original names
without modification.
"""
import logging
import os
import platform
from pathlib import Path

from flask import jsonify, request

from config.settings import config_manager
from core.metadata.registry import (
    get_spotify_client,
    get_primary_source,
    is_hydrabase_enabled,
)
from core.metadata.status import get_spotify_status

logger = logging.getLogger(__name__)


class _SpotifyClientProxy:
    """Resolves the global Spotify client lazily through core.metadata.registry."""

    def __getattr__(self, name):
        client = get_spotify_client()
        if client is None:
            raise AttributeError(name)
        return getattr(client, name)

    def __bool__(self):
        return get_spotify_client() is not None


class _TidalClientProxy:
    """Resolves the global Tidal client lazily via an injected getter so a
    Tidal re-auth that rebinds web_server.tidal_client is visible here."""

    def __getattr__(self, name):
        if _get_tidal_client is None:
            raise AttributeError(name)
        client = _get_tidal_client()
        if client is None:
            raise AttributeError(name)
        return getattr(client, name)

    def __bool__(self):
        if _get_tidal_client is None:
            return False
        return _get_tidal_client() is not None


spotify_client = _SpotifyClientProxy()
tidal_client = _TidalClientProxy()
_get_tidal_client = None  # injected via init()


# Injected at runtime via init().
SOULSYNC_VERSION = None
_DIRECT_RUN = None
_status_cache = None
qobuz_enrichment_worker = None
download_batches = None
sync_states = None
youtube_playlist_states = None
tidal_discovery_states = None
download_orchestrator = None
_log_path = None
_log_dir = None
app = None
get_database = None


def init(
    soulsync_version,
    direct_run,
    status_cache,
    qobuz_worker,
    download_batches_dict,
    sync_states_dict,
    youtube_playlist_states_dict,
    tidal_discovery_states_dict,
    download_orchestrator_obj,
    log_path,
    log_dir,
    flask_app,
    get_database_fn,
    tidal_client_getter,
):
    """Bind shared state/helpers from web_server."""
    global SOULSYNC_VERSION, _DIRECT_RUN, _status_cache, qobuz_enrichment_worker
    global download_batches, sync_states, youtube_playlist_states
    global tidal_discovery_states, download_orchestrator, _log_path, _log_dir
    global app, get_database, _get_tidal_client
    SOULSYNC_VERSION = soulsync_version
    _DIRECT_RUN = direct_run
    _status_cache = status_cache
    qobuz_enrichment_worker = qobuz_worker
    download_batches = download_batches_dict
    sync_states = sync_states_dict
    youtube_playlist_states = youtube_playlist_states_dict
    tidal_discovery_states = tidal_discovery_states_dict
    download_orchestrator = download_orchestrator_obj
    _log_path = log_path
    _log_dir = log_dir
    app = flask_app
    get_database = get_database_fn
    _get_tidal_client = tidal_client_getter


def _safe_check(fn, default=False):
    """Safely evaluate a check function, returning default on any error."""
    try:
        return fn()
    except Exception:
        return default


def get_debug_info():
    """Collect system diagnostics for troubleshooting support requests."""
    import sys
    import psutil
    import time
    from datetime import timedelta

    log_lines = request.args.get('lines', 20, type=int)
    log_lines = max(10, min(log_lines, 500))
    log_source = request.args.get('log', 'app')

    info = {}

    # App info
    info['version'] = SOULSYNC_VERSION
    info['os'] = f"{platform.system()} {platform.release()}"
    info['python'] = sys.version.split()[0]
    info['docker'] = os.path.exists('/.dockerenv')
    info['runner'] = 'gunicorn' if not _DIRECT_RUN else 'direct (python web_server.py)'

    # ffmpeg version
    try:
        import subprocess
        result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
        first_line = result.stdout.split('\n')[0] if result.stdout else ''
        # e.g. "ffmpeg version 6.1.1 Copyright ..."
        info['ffmpeg'] = first_line.split('Copyright')[0].replace('ffmpeg version', '').strip() if first_line else 'installed (version unknown)'
    except FileNotFoundError:
        info['ffmpeg'] = 'NOT INSTALLED'
    except Exception:
        info['ffmpeg'] = 'unknown'

    # Uptime
    start_time = getattr(app, 'start_time', time.time())
    uptime_seconds = time.time() - start_time
    info['uptime'] = str(timedelta(seconds=int(uptime_seconds)))

    # Paths
    download_path = config_manager.get('soulseek.download_path', './downloads')
    transfer_folder = config_manager.get('soulseek.transfer_path', './Transfer')
    staging_folder = config_manager.get('import.staging_path', '')
    info['paths'] = {
        'download_path': download_path,
        'download_path_exists': os.path.isdir(download_path) if download_path else False,
        'download_path_writable': os.access(download_path, os.W_OK) if download_path and os.path.isdir(download_path) else False,
        'transfer_folder': transfer_folder,
        'transfer_folder_exists': os.path.isdir(transfer_folder) if transfer_folder else False,
        'transfer_folder_writable': os.access(transfer_folder, os.W_OK) if transfer_folder and os.path.isdir(transfer_folder) else False,
        'staging_folder': staging_folder,
        'staging_folder_exists': os.path.isdir(staging_folder) if staging_folder else False,
    }
    # Music library paths (Settings > Library)
    music_paths = config_manager.get('library.music_paths', [])
    if isinstance(music_paths, list) and music_paths:
        info['paths']['music_library_paths'] = []
        for p in music_paths:
            if p and isinstance(p, str):
                info['paths']['music_library_paths'].append({
                    'path': p,
                    'exists': os.path.isdir(p),
                })
    # Music videos directory
    music_videos_path = config_manager.get('library.music_videos_path', '')
    if music_videos_path:
        info['paths']['music_videos_path'] = music_videos_path
        info['paths']['music_videos_path_exists'] = os.path.isdir(music_videos_path)

    # Services. `_status_cache` only carries 'media_server' and 'soulseek'
    # (no 'spotify' key) so anything we used to read from `spotify_cache`
    # silently defaulted to the missing-value fallback — that's the
    # "music_source: unknown" bug. Spotify status now comes from the
    # canonical `get_spotify_status` accessor; primary metadata source
    # comes from `get_primary_source` (which already accounts for the
    # auth-fallback chain — Spotify drops back to Deezer when not
    # authenticated).
    media_server_cache = _status_cache.get('media_server', {})
    soulseek_cache = _status_cache.get('soulseek', {})
    spotify_status = _safe_check(lambda: get_spotify_status(spotify_client=spotify_client), default={})
    if not isinstance(spotify_status, dict):
        spotify_status = {}
    info['services'] = {
        'music_source': _safe_check(get_primary_source, default='unknown') or 'unknown',
        'spotify_connected': bool(spotify_status.get('connected', False)),
        'spotify_rate_limited': bool(spotify_status.get('rate_limited', False)),
        'media_server_type': media_server_cache.get('type', 'none'),
        'media_server_connected': media_server_cache.get('connected', False),
        'soulseek_connected': soulseek_cache.get('connected', False),
        'download_source': config_manager.get('download_source.mode', 'hybrid'),
        'tidal_connected': _safe_check(lambda: bool(tidal_client and tidal_client.is_authenticated())),
        'qobuz_connected': _safe_check(lambda: bool(qobuz_enrichment_worker and qobuz_enrichment_worker.client and qobuz_enrichment_worker.client.is_authenticated())),
        'hydrabase_connected': _safe_check(is_hydrabase_enabled),
        # YouTube is URL-based via yt-dlp — no auth, always reachable as
        # long as the binary is installed. Surfaced so the debug dump
        # documents that YouTube is one of the available download sources
        # rather than implying it doesn't exist.
        'youtube_available': True,
    }
    # HiFi instance count — separate from connection status because each
    # instance is its own independent endpoint with its own auth state.
    info['services']['hifi_instance_count'] = _safe_check(
        lambda: len(get_database().get_hifi_instances()), default=0
    )
    # Always-available public metadata sources (no auth, no per-user
    # connection state). Listed so the debug dump reflects the full
    # metadata surface SoulSync queries from, not just the auth-gated ones.
    info['services']['always_available_metadata_sources'] = [
        'deezer', 'itunes', 'musicbrainz',
    ]

    # Enrichment workers
    workers = {}
    worker_names = ['musicbrainz', 'audiodb', 'deezer', 'spotify', 'itunes', 'lastfm', 'genius', 'discogs', 'tidal', 'qobuz']
    for name in worker_names:
        paused_key = f'{name}_enrichment_paused'
        workers[name] = 'paused' if config_manager.get(paused_key, False) else 'active'
    info['enrichment_workers'] = workers

    # Library stats — use same method as dashboard (filters by active server)
    try:
        db = get_database()
        lib_stats = db.get_database_info_for_server()
        info['library'] = {
            'artists': lib_stats.get('artists', 0),
            'albums': lib_stats.get('albums', 0),
            'tracks': lib_stats.get('tracks', 0),
        }
    except Exception:
        info['library'] = {'artists': 0, 'albums': 0, 'tracks': 0}

    # Watchlist count
    try:
        db = get_database()
        info['watchlist_count'] = db.get_watchlist_count()
    except Exception:
        info['watchlist_count'] = 0

    # Wishlist pending count
    try:
        db = get_database()
        info['wishlist_count'] = db.get_wishlist_count()
    except Exception:
        info['wishlist_count'] = 0

    # Automation count
    try:
        db = get_database()
        automations = db.get_automations()
        info['automations'] = {
            'total': len(automations),
            'enabled': len([a for a in automations if a.get('enabled', False)]),
        }
    except Exception:
        info['automations'] = {'total': 0, 'enabled': 0}

    # Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation)
    try:
        active_downloads = len([bid for bid, bd in list(download_batches.items()) if bd.get('phase') == 'downloading'])
    except Exception:
        active_downloads = 0
    active_syncs = 0
    try:
        for _pid, ss in list(sync_states.items()):
            if ss.get('status') == 'syncing':
                active_syncs += 1
        for _uh, st in list(youtube_playlist_states.items()):
            if st.get('phase') == 'syncing':
                active_syncs += 1
        for _pid, st in list(tidal_discovery_states.items()):
            if st.get('phase') == 'syncing':
                active_syncs += 1
    except Exception as e:
        logger.debug("count active syncs failed: %s", e)
    info['active_downloads'] = active_downloads
    info['active_syncs'] = active_syncs

    # Config settings relevant to troubleshooting
    source_mode = config_manager.get('download_source.mode', 'hybrid')
    info['config'] = {
        'source_mode': source_mode,
        'quality_profile': config_manager.get('download_source.quality_profile', 'default'),
        'organization_template': config_manager.get('organization.folder_template', ''),
        'post_processing_enabled': config_manager.get('post_processing.enabled', True),
        'acoustid_enabled': bool(config_manager.get('acoustid.api_key', '')),
        'auto_scan_enabled': config_manager.get('watchlist.auto_scan', False),
        'm3u_export_enabled': config_manager.get('m3u.enabled', False),
        'log_level': config_manager.get('logging.level', 'INFO'),
        'primary_metadata_source': config_manager.get('metadata.fallback_source', 'deezer'),
        'lossy_copy_enabled': config_manager.get('post_processing.lossy_copy.enabled', False),
        'lossy_copy_format': config_manager.get('post_processing.lossy_copy.format', 'mp3'),
        'lossy_copy_bitrate': config_manager.get('post_processing.lossy_copy.bitrate', 320),
        'allow_duplicate_tracks': config_manager.get('library.allow_duplicate_tracks', False),
        'replace_lower_quality': config_manager.get('import.replace_lower_quality', False),
        'auto_import_enabled': config_manager.get('import.auto_import_enabled', False),
    }
    # Hybrid source priority order
    if source_mode == 'hybrid':
        info['config']['hybrid_sources'] = config_manager.get('download_source.hybrid_order', [])
    # Discogs connection status
    info['services']['discogs_connected'] = bool(config_manager.get('discogs.token', ''))

    # Download client init failures
    info['download_client_failures'] = []
    if download_orchestrator and hasattr(download_orchestrator, '_init_failures'):
        info['download_client_failures'] = download_orchestrator._init_failures
    elif not download_orchestrator:
        info['download_client_failures'] = ['ALL (orchestrator failed to initialize)']

    # API rate monitor — current calls/min, 24h totals, peaks, rate limit events
    try:
        from core.api_call_tracker import api_call_tracker
        # `get_spotify_status` is already imported at module level. A
        # local re-import here would make Python treat the name as a
        # function-scoped local for the WHOLE body, breaking the lambda
        # at the top of get_debug_info that closes over the module-level
        # binding (Python 3.12 NameError on free variables).
        rates = api_call_tracker.get_all_rates()
        info['api_rates'] = rates
        # Rich 24h debug summary with peaks, totals, per-endpoint breakdown, events
        info['api_debug_summary'] = api_call_tracker.get_debug_summary()
        # Spotify rate limit details
        spotify_status = get_spotify_status(spotify_client=spotify_client)
        rl_info = spotify_status.get('rate_limit')
        if spotify_status.get('rate_limited') and rl_info:
            info['spotify_rate_limit'] = {
                'active': True,
                'remaining_seconds': rl_info.get('remaining_seconds', 0),
                'retry_after': rl_info.get('retry_after', 0),
                'endpoint': rl_info.get('endpoint', ''),
                'expires_at': rl_info.get('expires_at', ''),
            }
        else:
            info['spotify_rate_limit'] = {'active': False}
    except Exception:
        info['api_rates'] = {}
        info['api_debug_summary'] = {}
        info['spotify_rate_limit'] = {'active': False}

    # Database size
    db_path = os.path.join('database', 'music_library.db')
    if os.path.exists(db_path):
        db_size_mb = os.path.getsize(db_path) / (1024 * 1024)
        info['database_size'] = f"{db_size_mb:.1f} MB"
    else:
        info['database_size'] = 'not found'

    # Memory & CPU
    process = psutil.Process(os.getpid())
    mem = process.memory_info()
    info['memory_usage'] = f"{mem.rss / (1024 * 1024):.0f} MB"
    info['system_memory'] = f"{psutil.virtual_memory().percent}%"
    try:
        info['cpu_percent'] = f"{process.cpu_percent(interval=0.1):.1f}%"
    except Exception:
        info['cpu_percent'] = 'unknown'
    info['thread_count'] = process.num_threads()

    # Log lines
    log_map = {
        'app': Path(_log_path),
        'acoustid': _log_dir / 'acoustid.log',
        'post_processing': _log_dir / 'post_processing.log',
        'source_reuse': _log_dir / 'source_reuse.log',
    }
    log_path = log_map.get(log_source, log_map['app'])
    info['log_source'] = log_source
    info['log_lines_requested'] = log_lines
    info['recent_logs'] = []
    if os.path.exists(log_path):
        try:
            with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
                lines = f.readlines()
                info['recent_logs'] = [line.rstrip() for line in lines[-log_lines:]]
        except Exception:
            info['recent_logs'] = ['(could not read log file)']

    # Available log files
    info['available_logs'] = []
    logs_dir = 'logs'
    if os.path.isdir(logs_dir):
        for fname in sorted(os.listdir(logs_dir)):
            if fname.endswith('.log'):
                fpath = os.path.join(logs_dir, fname)
                size_kb = os.path.getsize(fpath) / 1024
                info['available_logs'].append({
                    'name': fname.replace('.log', ''),
                    'file': fname,
                    'size': f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB",
                })

    return jsonify(info)
