"""
sync_manager.py — Synkronisering mellem lokal database og server. v0.9
"""
import json
import logging
import sqlite3
import threading
import urllib.request
import urllib.error
from pathlib import Path

logger = logging.getLogger(__name__)


class SyncManager:

    def __init__(self, api_url: str = "", db_path: str = "",
                 server_url: str = "", token: str | None = None):
        # Støt både api_url og server_url som parameter-navn
        self._api_url = (api_url or server_url).rstrip("/")
        self._db_path = db_path
        self._token: str | None = token

    def set_token(self, token: str):
        self._token = token

    # ── HTTP ──────────────────────────────────────────────────────────────────

    def _post(self, path: str, data: dict) -> dict:
        body = json.dumps(data).encode()
        req  = urllib.request.Request(
            f"{self._api_url}{path}",
            data=body,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self._token}",
            },
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as e:
            detail = e.read().decode()
            raise Exception(f"HTTP {e.code}: {detail}")

    def _get(self, path: str) -> dict:
        req = urllib.request.Request(
            f"{self._api_url}{path}",
            headers={"Authorization": f"Bearer {self._token}"},
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as e:
            detail = e.read().decode()
            raise Exception(f"HTTP {e.code}: {detail}")

    # ── Push ──────────────────────────────────────────────────────────────────

    def push(self, on_done=None, on_error=None):
        def _run():
            try:
                payload = self._build_push_payload()
                logger.info(f"Push: {len(payload['songs'])} sange, "
                            f"{len(payload['playlists'])} playlister")
                result = self._post("/sync/push", payload)
                self._save_server_ids(
                    result.get("song_id_map", {}),
                    result.get("playlist_id_map", {}),
                )
                logger.info(f"Push OK: {result.get('songs_synced','?')} sange synkroniseret")
                if on_done:
                    on_done(result)
            except Exception as e:
                logger.error(f"Push fejl: {e}", exc_info=True)
                if on_error:
                    on_error(str(e))
        threading.Thread(target=_run, daemon=True).start()

    # ── Push + Pull ───────────────────────────────────────────────────────────

    def push_and_pull(self, on_done=None, on_error=None):
        def _run():
            try:
                # 1. Push
                payload = self._build_push_payload()
                deleted = payload.get("deleted_playlists", [])
                logger.info(f"Sync push — {len(payload['songs'])} sange, "
                            f"{len(payload['playlists'])} playlister, "
                            f"sletter {len(deleted)}: {deleted}")
                push_result = self._post("/sync/push", payload)
                self._save_server_ids(
                    push_result.get("song_id_map", {}),
                    push_result.get("playlist_id_map", {}),
                )
                logger.info(f"Push svar: status={push_result.get('status')}, "
                            f"sange={push_result.get('songs_synced', 0)}, "
                            f"playlister={push_result.get('playlists_synced', 0)}")

                # 2. Pull
                pull_result = self._get("/sync/pull")
                pl_names = [p.get("name") for p in pull_result.get("my_playlists", [])]
                logger.info(f"Pull modtog {len(pl_names)} playlister: {pl_names}")
                self._apply_pull(pull_result)

                # 3. Fjern soft-slettede permanent efter succesfuld sync
                if deleted:
                    conn = sqlite3.connect(self._db_path, timeout=10)
                    conn.execute("PRAGMA journal_mode=WAL")
                    conn.execute(
                        "DELETE FROM playlists WHERE is_deleted=1 AND api_project_id IS NOT NULL"
                    )
                    conn.commit()
                    conn.close()
                    logger.info("Soft-slettede playlister fjernet lokalt efter sync")

                pl_count = len(pull_result.get("my_playlists", []))
                logger.info(f"Sync OK — {len(payload['songs'])} sange, "
                            f"{len(payload['playlists'])} playlister, "
                            f"{pl_count} server-playlister")
                if on_done:
                    on_done({"push": push_result, "pull": pull_result})
            except Exception as e:
                logger.error(f"push_and_pull fejl: {e}", exc_info=True)
                if on_error:
                    on_error(str(e))
        threading.Thread(target=_run, daemon=True).start()

    # ── Byg payload ───────────────────────────────────────────────────────────

    def _build_push_payload(self) -> dict:
        conn = sqlite3.connect(self._db_path, timeout=10)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")

        # Sange (dem der har filer — altså kendes lokalt)
        songs = []
        for row in conn.execute("""
            SELECT DISTINCT s.id, s.title, s.artist, s.album,
                   s.bpm, s.duration_sec, s.mbid, s.acoustid, s.server_synced
            FROM songs s
            JOIN files f ON f.song_id = s.id AND f.file_missing = 0
        """).fetchall():
            songs.append({
                "local_id":     row["id"],
                "title":        row["title"] or "",
                "artist":       row["artist"] or "",
                "album":        row["album"] or "",
                "bpm":          row["bpm"] or 0,
                "duration_sec": row["duration_sec"] or 0,
                "mbid":         row["mbid"] or "",
                "acoustid":     row["acoustid"] or "",
            })

        # Danse
        dances = []
        for row in conn.execute("""
            SELECT d.name, dl.name as level_name, d.choreographer,
                   d.video_url, d.stepsheet_url, d.notes
            FROM dances d LEFT JOIN dance_levels dl ON dl.id = d.level_id
        """).fetchall():
            dances.append({
                "name":          row["name"] or "",
                "level_name":    row["level_name"] or "",
                "choreographer": row["choreographer"] or "",
                "video_url":     row["video_url"] or "",
                "stepsheet_url": row["stepsheet_url"] or "",
                "notes":         row["notes"] or "",
            })

        # Dans-tags
        song_dances = []
        for row in conn.execute("""
            SELECT sd.song_id, d.name as dance_name,
                   dl.name as level_name, sd.dance_order
            FROM song_dances sd
            JOIN dances d ON d.id = sd.dance_id
            LEFT JOIN dance_levels dl ON dl.id = d.level_id
        """).fetchall():
            song_dances.append({
                "song_local_id": row["song_id"],
                "dance_name":    row["dance_name"],
                "level_name":    row["level_name"] or "",
                "dance_order":   row["dance_order"],
            })

        # Alternativ-danse
        song_alts = []
        for row in conn.execute("""
            SELECT sad.song_id, d.name as dance_name,
                   dl.name as level_name, sad.note, sad.user_rating
            FROM song_alt_dances sad
            JOIN dances d ON d.id = sad.dance_id
            LEFT JOIN dance_levels dl ON dl.id = d.level_id
        """).fetchall():
            song_alts.append({
                "song_local_id": row["song_id"],
                "dance_name":    row["dance_name"],
                "level_name":    row["level_name"] or "",
                "note":          row["note"] or "",
                "user_rating":   row["user_rating"],
            })

        # Playlister — alle ikke-slettede
        playlists = []
        for pl in conn.execute("""
            SELECT id, name, description, tags, api_project_id
            FROM playlists
            WHERE name != '__aktiv__' AND is_deleted = 0
        """).fetchall():
            pl_songs = []
            for ps in conn.execute("""
                SELECT s.id as song_id, s.title, s.artist,
                       ps.position, ps.status, ps.is_workshop, ps.dance_override
                FROM playlist_songs ps
                JOIN songs s ON s.id = ps.song_id
                WHERE ps.playlist_id=? ORDER BY ps.position
            """, (pl["id"],)).fetchall():
                pl_songs.append({
                    "song_local_id":  ps["song_id"],
                    "song_title":     ps["title"] or "",
                    "song_artist":    ps["artist"] or "",
                    "position":       int(ps["position"] or 1),
                    "status":         ps["status"] or "pending",
                    "is_workshop":    bool(ps["is_workshop"]),
                    "dance_override": ps["dance_override"] or "",
                })
            # Brug api_project_id som local_id hvis kendt
            local_id = pl["api_project_id"] or pl["id"]
            playlists.append({
                "local_id":    local_id,
                "name":        pl["name"],
                "description": pl["description"] or "",
                "tags":        pl["tags"] or "",
                "visibility":  "private",
                "songs":       pl_songs,
            })

        # Slettede playlister
        deleted = [
            row["api_project_id"]
            for row in conn.execute(
                "SELECT api_project_id FROM playlists "
                "WHERE is_deleted=1 AND api_project_id IS NOT NULL AND api_project_id != ''"
            ).fetchall()
        ]

        # Alle sang-IDs der pusher dans-tags fuldt (inkl. dem med 0 tags)
        all_song_ids = [s["local_id"] for s in songs]

        conn.close()
        return {
            "songs":                    songs,
            "dances":                   dances,
            "song_dances":              song_dances,
            "song_alts":                song_alts,
            "playlists":                playlists,
            "deleted_playlists":        deleted,
            "songs_with_dances_synced": all_song_ids,
        }

    # ── Gem server-IDs ────────────────────────────────────────────────────────

    def _save_server_ids(self, song_id_map: dict, playlist_id_map: dict):
        """
        Gem server-IDs lokalt.
        song_id_map:     lokal_song_id → server_song_id
        playlist_id_map: lokal_pl_id   → server_pl_id
        """
        if not song_id_map and not playlist_id_map:
            return
        conn = sqlite3.connect(self._db_path, timeout=10)
        conn.execute("PRAGMA journal_mode=WAL")

        # Sange: hvis server gav et andet ID end det lokale, opdater
        for local_id, server_id in song_id_map.items():
            if local_id != server_id:
                # Tjek om server-ID allerede eksisterer
                existing = conn.execute(
                    "SELECT id FROM songs WHERE id=?", (server_id,)
                ).fetchone()
                if not existing:
                    # Opdater lokal sang til server-ID
                    conn.execute(
                        "UPDATE songs SET id=?, server_synced=1 WHERE id=?",
                        (server_id, local_id)
                    )
                    # Opdater referencer
                    conn.execute(
                        "UPDATE files SET song_id=? WHERE song_id=?",
                        (server_id, local_id)
                    )
                    conn.execute(
                        "UPDATE playlist_songs SET song_id=? WHERE song_id=?",
                        (server_id, local_id)
                    )
                    conn.execute(
                        "UPDATE song_dances SET song_id=? WHERE song_id=?",
                        (server_id, local_id)
                    )
                    conn.execute(
                        "UPDATE song_alt_dances SET song_id=? WHERE song_id=?",
                        (server_id, local_id)
                    )
                else:
                    conn.execute(
                        "UPDATE songs SET server_synced=1 WHERE id=?", (local_id,)
                    )

        # Playlister
        for local_id, server_id in playlist_id_map.items():
            conn.execute(
                "UPDATE playlists SET api_project_id=? WHERE id=? OR api_project_id=?",
                (server_id, local_id, local_id)
            )

        conn.commit()
        conn.close()

    # ── Anvend pull ───────────────────────────────────────────────────────────

    def _apply_pull(self, data: dict):
        """Gem server-data lokalt."""
        import uuid
        conn = sqlite3.connect(self._db_path, timeout=10)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")

        try:
            # Synkroniser danse fra server — opret nye, opdater eksisterende
            for d in data.get("dances", []):
                if not d.get("name"):
                    continue
                choreo = d.get("choreographer", "") or ""
                existing = conn.execute(
                    "SELECT id FROM dances WHERE name=? COLLATE NOCASE "
                    "AND choreographer=? LIMIT 1",
                    (d["name"], choreo)
                ).fetchone()
                if existing:
                    conn.execute("""
                        UPDATE dances SET
                            video_url     = CASE WHEN video_url=''     THEN ? ELSE video_url END,
                            stepsheet_url = CASE WHEN stepsheet_url='' THEN ? ELSE stepsheet_url END
                        WHERE id=?
                    """, (d.get("video_url",""), d.get("stepsheet_url",""), existing["id"]))
                else:
                    conn.execute(
                        "INSERT OR IGNORE INTO dances (name, choreographer, video_url, stepsheet_url, notes) "
                        "VALUES (?,?,?,?,?)",
                        (d["name"], choreo,
                         d.get("video_url",""), d.get("stepsheet_url",""), d.get("notes",""))
                    )

            # Hent soft-slettede server-IDs så vi springer dem over
            deleted_server_ids = {
                row["api_project_id"]
                for row in conn.execute(
                    "SELECT api_project_id FROM playlists "
                    "WHERE is_deleted=1 AND api_project_id IS NOT NULL"
                ).fetchall()
            }

            # Importer egne playlister
            for pl in data.get("my_playlists", []):
                server_id = pl.get("server_id")
                name      = pl.get("name", "")
                if not server_id or not name:
                    continue
                if server_id in deleted_server_ids:
                    continue

                existing = conn.execute(
                    "SELECT id FROM playlists WHERE api_project_id=?", (server_id,)
                ).fetchone()

                if existing:
                    pl_id = existing["id"]
                    conn.execute(
                        "UPDATE playlists SET name=? WHERE id=?", (name, pl_id)
                    )
                else:
                    pl_id = str(uuid.uuid4())
                    conn.execute(
                        "INSERT INTO playlists (id, name, description, api_project_id, is_linked, server_permission) "
                        "VALUES (?,?,?,?,1,'edit')",
                        (pl_id, name, pl.get("description",""), server_id)
                    )

                # Genindlæs sange
                conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
                position = 1
                songs_from_server = pl.get("songs", [])
                logger.info(f"Pull: liste '{name}' har {len(songs_from_server)} sange")

                for song_data in songs_from_server:
                    server_song_id = song_data.get("song_id", "")
                    title  = song_data.get("title", "")
                    artist = song_data.get("artist", "")
                    mbid   = song_data.get("mbid", "")
                    acoustid = song_data.get("acoustid", "")

                    if not title and not server_song_id:
                        continue

                    # Find eller opret sang lokalt
                    local_song_id = self._find_or_create_song_local(
                        conn, server_song_id, title, artist,
                        mbid=mbid, acoustid=acoustid,
                        bpm=song_data.get("bpm", 0),
                        duration_sec=song_data.get("duration_sec", 0),
                    )

                    # Find tilgængelig fil til denne sang
                    file_row = conn.execute(
                        "SELECT id FROM files WHERE song_id=? AND file_missing=0 LIMIT 1",
                        (local_song_id,)
                    ).fetchone()
                    file_id = file_row["id"] if file_row else None

                    conn.execute("""
                        INSERT INTO playlist_songs
                        (id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override)
                        VALUES (?,?,?,?,?,?,?,?)
                    """, (str(uuid.uuid4()), pl_id, local_song_id, file_id, position,
                          song_data.get("status","pending"),
                          1 if song_data.get("is_workshop") else 0,
                          song_data.get("dance_override","") or ""))
                    position += 1

            # Importer delte playlister
            for pl in data.get("shared", []):
                server_id = pl.get("server_id")
                name      = pl.get("name", "")
                owner     = pl.get("owner", "?")
                if not server_id or not name:
                    continue

                existing = conn.execute(
                    "SELECT id FROM playlists WHERE api_project_id=?", (server_id,)
                ).fetchone()

                if existing:
                    pl_id = existing["id"]
                    conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
                else:
                    pl_id = str(uuid.uuid4())
                    conn.execute(
                        "INSERT INTO playlists (id, name, description, api_project_id, is_linked, server_permission) "
                        "VALUES (?,?,?,?,1,'view')",
                        (pl_id, f"{name} ({owner})", "", server_id)
                    )

                position = 1
                for song_data in pl.get("songs", []):
                    server_song_id = song_data.get("song_id", "")
                    title  = song_data.get("title", "")
                    artist = song_data.get("artist", "")
                    if not title and not server_song_id:
                        continue

                    local_song_id = self._find_or_create_song_local(
                        conn, server_song_id, title, artist,
                        mbid=song_data.get("mbid", ""),
                        acoustid=song_data.get("acoustid", ""),
                    )
                    file_row = conn.execute(
                        "SELECT id FROM files WHERE song_id=? AND file_missing=0 LIMIT 1",
                        (local_song_id,)
                    ).fetchone()
                    file_id = file_row["id"] if file_row else None

                    conn.execute("""
                        INSERT INTO playlist_songs
                        (id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override)
                        VALUES (?,?,?,?,?,?,?,?)
                    """, (str(uuid.uuid4()), pl_id, local_song_id, file_id, position,
                          song_data.get("status","pending"),
                          1 if song_data.get("is_workshop") else 0,
                          song_data.get("dance_override","") or ""))
                    position += 1

            # Gem community alternativ-danse lokalt
            conn.execute(
                "CREATE TABLE IF NOT EXISTS community_alt_dances ("
                "id TEXT PRIMARY KEY, song_id TEXT NOT NULL, dance_id INTEGER NOT NULL, "
                "avg_rating REAL NOT NULL DEFAULT 0, rating_count INTEGER NOT NULL DEFAULT 0, "
                "my_rating INTEGER, UNIQUE(song_id, dance_id))"
            )
            for ca in data.get("community_alts", []):
                if not ca.get("dance_name"):
                    continue
                song_row = None
                if ca.get("song_mbid"):
                    song_row = conn.execute(
                        "SELECT id FROM songs WHERE mbid=?", (ca["song_mbid"],)
                    ).fetchone()
                if not song_row and ca.get("song_title"):
                    song_row = conn.execute(
                        "SELECT id FROM songs WHERE title=? AND artist=?",
                        (ca["song_title"], ca.get("song_artist", ""))
                    ).fetchone()
                if not song_row:
                    continue
                song_id = song_row["id"]
                dance_row = conn.execute(
                    "SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1",
                    (ca["dance_name"],)
                ).fetchone()
                if not dance_row:
                    cur = conn.execute(
                        "INSERT OR IGNORE INTO dances (name) VALUES (?)", (ca["dance_name"],)
                    )
                    dance_id = cur.lastrowid
                else:
                    dance_id = dance_row["id"]
                if not dance_id:
                    continue
                conn.execute(
                    "INSERT INTO community_alt_dances "
                    "(id, song_id, dance_id, avg_rating, rating_count, my_rating) "
                    "VALUES (?,?,?,?,?,?) "
                    "ON CONFLICT(song_id, dance_id) DO UPDATE SET "
                    "avg_rating=excluded.avg_rating, rating_count=excluded.rating_count, "
                    "my_rating=COALESCE(excluded.my_rating, my_rating)",
                    (str(uuid.uuid4()), song_id, dance_id,
                     ca.get("avg_rating", 0), ca.get("rating_count", 0),
                     ca.get("my_rating"))
                )

            # Importer sang-dans tags fra server
            for st in data.get("song_tags", []):
                server_song_id = st.get("song_id", "")
                dance_name     = st.get("dance_name", "")
                dance_order    = st.get("dance_order", 1)
                choreo         = st.get("choreographer", "") or ""
                if not server_song_id or not dance_name:
                    continue

                # Find lokal sang
                local_song = conn.execute(
                    "SELECT id FROM songs WHERE id=?", (server_song_id,)
                ).fetchone()
                if not local_song:
                    continue

                # Find dans
                dance_row = conn.execute(
                    "SELECT id FROM dances WHERE name=? COLLATE NOCASE "
                    "AND choreographer=? LIMIT 1",
                    (dance_name, choreo)
                ).fetchone()
                if not dance_row:
                    cur = conn.execute(
                        "INSERT OR IGNORE INTO dances (name, choreographer) VALUES (?,?)",
                        (dance_name, choreo)
                    )
                    dance_id = cur.lastrowid
                else:
                    dance_id = dance_row["id"]

                # Tilføj sang-dans tag hvis ikke allerede der
                existing_sd = conn.execute(
                    "SELECT id FROM song_dances WHERE song_id=? AND dance_id=?",
                    (server_song_id, dance_id)
                ).fetchone()
                if not existing_sd:
                    conn.execute(
                        "INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) "
                        "VALUES (?,?,?,?)",
                        (str(uuid.uuid4()), server_song_id, dance_id, dance_order)
                    )

            conn.commit()

        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def _find_or_create_song_local(self, conn, server_song_id: str, title: str,
                                    artist: str = "", mbid: str = "",
                                    acoustid: str = "", bpm: int = 0,
                                    duration_sec: int = 0) -> str:
        """Find eller opret sang lokalt. Returnerer lokal song_id."""
        import uuid

        # Match på server-ID
        if server_song_id:
            row = conn.execute(
                "SELECT id FROM songs WHERE id=?", (server_song_id,)
            ).fetchone()
            if row:
                return row["id"]

        # Match på MBID
        if mbid:
            row = conn.execute(
                "SELECT id FROM songs WHERE mbid=?", (mbid,)
            ).fetchone()
            if row:
                return row["id"]

        # Match på AcoustID
        if acoustid:
            row = conn.execute(
                "SELECT id FROM songs WHERE acoustid=?", (acoustid,)
            ).fetchone()
            if row:
                return row["id"]

        # Match på titel + artist
        if title:
            row = conn.execute(
                "SELECT id FROM songs WHERE title=? AND artist=?", (title, artist)
            ).fetchone()
            if row:
                return row["id"]

        # Opret ny — brug server-ID hvis tilgængeligt
        new_id = server_song_id or str(uuid.uuid4())
        conn.execute(
            "INSERT INTO songs (id, title, artist, bpm, duration_sec, mbid, acoustid, server_synced) "
            "VALUES (?,?,?,?,?,?,?,1)",
            (new_id, title, artist, bpm, duration_sec, mbid or None, acoustid or None)
        )
        logger.info(f"Pull: oprettet sang '{title}' ({new_id})")
        return new_id