"""Dead File Cleaner Job — finds DB track entries where the file no longer exists."""

import os

from core.library.path_resolver import resolve_library_file_path
from core.repair_jobs import register_job
from core.repair_jobs.base import JobContext, JobResult, RepairJob
from utils.logging_config import get_logger

logger = get_logger("repair_job.dead_files")


def _resolve_file_path(file_path, transfer_folder, download_folder=None, config_manager=None):
    """Backwards-compat wrapper. Use ``resolve_library_file_path`` directly."""
    return resolve_library_file_path(
        file_path,
        transfer_folder=transfer_folder,
        download_folder=download_folder,
        config_manager=config_manager,
    )


@register_job
class DeadFileCleanerJob(RepairJob):
    job_id = 'dead_file_cleaner'
    display_name = 'Dead File Cleaner'
    description = 'Finds database entries pointing to missing files'
    help_text = (
        'Checks every track in your database to verify the actual audio file still exists '
        'on disk. If a file has been moved, renamed, or deleted outside of SoulSync, the '
        'database entry becomes a "dead" reference.\n\n'
        'Each dead reference is reported as a finding. You can then resolve it by re-downloading '
        'the track or dismiss it to clean up the database entry.\n\n'
        'This job only scans and reports — it never deletes database entries automatically.'
    )
    icon = 'repair-icon-deadfile'
    default_enabled = True
    default_interval_hours = 24
    default_settings = {
        # Mass-false-positive guard: if at least this fraction of tracks resolve
        # to no file on disk, treat it as a path-mapping/mount problem (SoulSync
        # can't SEE the library — e.g. Docker, or library.music_paths unset for
        # this environment) rather than thousands of individually-deleted files,
        # and abort without creating findings. Mirrors the transfer-folder abort.
        'max_unresolved_fraction': 0.5,
        # ...but only once the library is at least this big — a small library can
        # legitimately have a high dead fraction.
        'min_tracks_for_guard': 25,
    }
    auto_fix = False

    def scan(self, context: JobContext) -> JobResult:
        result = JobResult()

        # Safety: abort if transfer folder doesn't exist — prevents mass false positives
        if not context.transfer_folder or not os.path.isdir(context.transfer_folder):
            logger.error("Transfer folder not found: %s — aborting dead file scan to avoid false positives",
                         context.transfer_folder)
            result.errors += 1
            if context.report_progress:
                context.report_progress(
                    phase='Aborted — transfer folder not found',
                    log_line=f'Transfer folder does not exist: {context.transfer_folder}',
                    log_type='error'
                )
            return result

        # Fetch all tracks with file paths, joining to get artist/album names
        tracks = []
        conn = None
        try:
            conn = context.db._get_connection()
            cursor = conn.cursor()
            cursor.execute("""
                SELECT t.id, t.title, ar.name, al.title, t.file_path,
                       al.thumb_url, ar.thumb_url
                FROM tracks t
                LEFT JOIN artists ar ON ar.id = t.artist_id
                LEFT JOIN albums al ON al.id = t.album_id
                WHERE t.file_path IS NOT NULL AND t.file_path != ''
            """)
            tracks = cursor.fetchall()
        except Exception as e:
            logger.error("Error fetching tracks from DB: %s", e, exc_info=True)
            result.errors += 1
            return result
        finally:
            if conn:
                conn.close()

        total = len(tracks)
        if context.update_progress:
            context.update_progress(0, total)

        # Get download folder for path resolution fallback
        download_folder = None
        if context.config_manager:
            download_folder = context.config_manager.get('soulseek.download_path', '')

        # Mass-false-positive guard thresholds (see default_settings).
        max_unresolved_fraction = 0.5
        min_tracks_for_guard = 25
        if context.config_manager:
            try:
                max_unresolved_fraction = float(context.config_manager.get(
                    self.get_config_key('max_unresolved_fraction'), 0.5))
            except (TypeError, ValueError):
                max_unresolved_fraction = 0.5
            try:
                min_tracks_for_guard = int(context.config_manager.get(
                    self.get_config_key('min_tracks_for_guard'), 25))
            except (TypeError, ValueError):
                min_tracks_for_guard = 25

        if context.report_progress:
            context.report_progress(phase=f'Checking {total} tracks...', total=total)

        # Collect unresolvable tracks first; decide whether they're genuine dead
        # files or a systemic path problem AFTER the full pass (below). A "None"
        # from the resolver means "couldn't find it at any known base dir" — which
        # for a mis-mounted library is EVERY track, not a real deletion.
        dead_rows = []

        for i, row in enumerate(tracks):
            if context.check_stop():
                return result
            if i % 200 == 0 and context.wait_if_paused():
                return result

            track_id, title, artist_name, album_title, file_path, album_thumb, artist_thumb = row
            result.scanned += 1

            if context.report_progress and i % 50 == 0:
                context.report_progress(
                    scanned=i + 1, total=total,
                    phase=f'Checking {i + 1} / {total}',
                    log_line=f'Checking: {title or "Unknown"} — {artist_name or "Unknown"}',
                    log_type='info'
                )

            # Use the same path resolution logic as library playback
            resolved = _resolve_file_path(file_path, context.transfer_folder, download_folder,
                                           config_manager=context.config_manager)

            if resolved is None:
                dead_rows.append(row)

            if context.update_progress and (i + 1) % 100 == 0:
                context.update_progress(i + 1, total)

        # Mass-false-positive guard: a large fraction of unresolvable paths almost
        # always means SoulSync can't SEE the library (Docker mount, or
        # Settings → Library → Music Paths not set for this environment), NOT that
        # thousands of files were individually deleted. Refuse to flag and say so
        # — same principle as the transfer-folder abort above. (#828: a Plex-on-
        # macOS user in Docker had all 5,250 tracks flagged because their stored
        # /Volumes/... paths don't exist inside the container.)
        if (dead_rows
                and result.scanned >= min_tracks_for_guard
                and len(dead_rows) >= result.scanned * max_unresolved_fraction):
            logger.error(
                "Dead file scan: %d/%d tracks unresolvable (>= %.0f%%) — aborting without "
                "creating findings; this is a path-mapping/mount problem, not deleted files.",
                len(dead_rows), result.scanned, max_unresolved_fraction * 100)
            result.errors += 1
            if context.report_progress:
                context.report_progress(
                    phase='Aborted — too many unreachable paths',
                    log_line=(f"{len(dead_rows)} of {result.scanned} tracks point to paths SoulSync "
                              f"can't reach — almost always a path-mapping issue (Docker mount, or "
                              f"Settings → Library → Music Paths), not deleted files. No findings created."),
                    log_type='error'
                )
            if context.update_progress:
                context.update_progress(total, total)
            return result

        # A small fraction unresolvable — treat as genuine dead files and report.
        for row in dead_rows:
            track_id, title, artist_name, album_title, file_path, album_thumb, artist_thumb = row
            if context.report_progress:
                context.report_progress(
                    log_line=f'Missing: {title or "Unknown"} — {os.path.basename(file_path)}',
                    log_type='error'
                )
            if context.create_finding:
                try:
                    inserted = context.create_finding(
                        job_id=self.job_id,
                        finding_type='dead_file',
                        severity='warning',
                        entity_type='track',
                        entity_id=str(track_id),
                        file_path=file_path,
                        title=f'Missing file: {title or "Unknown"}',
                        description=f'Track "{title}" by {artist_name or "Unknown"} points to a file that no longer exists',
                        details={
                            'track_id': track_id,
                            'title': title,
                            'artist': artist_name,
                            'album': album_title,
                            'original_path': file_path,
                            'album_thumb_url': album_thumb or None,
                            'artist_thumb_url': artist_thumb or None,
                        }
                    )
                    if inserted:
                        result.findings_created += 1
                    else:
                        result.findings_skipped_dedup += 1
                except Exception as e:
                    logger.debug("Error creating dead file finding for track %s: %s", track_id, e)
                    result.errors += 1

        if context.update_progress:
            context.update_progress(total, total)

        logger.info("Dead file scan: %d tracks checked, %d missing files found",
                     result.scanned, result.findings_created)
        return result

    def estimate_scope(self, context: JobContext) -> int:
        conn = None
        try:
            conn = context.db._get_connection()
            cursor = conn.cursor()
            cursor.execute("SELECT COUNT(*) FROM tracks WHERE file_path IS NOT NULL AND file_path != ''")
            row = cursor.fetchone()
            return row[0] if row else 0
        except Exception:
            return 0
        finally:
            if conn:
                conn.close()
