import requests
import json
import base64
import re
import os
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed

# UI & Tags
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
# Quitamos Panel para que sea texto plano
from mutagen.flac import FLAC, Picture
from mutagen.id3 import ID3, TIT2, TPE1, TALB, TYER, TRCK, APIC, TCON

console = Console()

class TiDown:
    """
    Cliente Maestro TiDown.
    Maneja Búsqueda y Descarga (MP3/FLAC/HiRes) en una sola sesión.
    """
    
    CALIDADES_MAP = {
        "MP3": "HIGH",              # AAC 320 -> MP3
        "FLAC": "LOSSLESS",         # FLAC 16-bit
        "HIRES": "HI_RES_LOSSLESS"  # FLAC 24-bit
    }

    def __init__(self, token, client_id="CzET4vdadNUFQ5JU"):
        self.token = token if token.startswith("Bearer") else f"Bearer {token}"
        self.client_id = client_id
        self.headers = {
            "Authorization": self.token,
            "x-tidal-token": self.client_id,
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Accept": "application/json"
        }
        self.country_code = self._detectar_pais()

    def _detectar_pais(self):
        try:
            r = requests.get("https://api.tidal.com/v1/sessions", headers=self.headers)
            return r.json().get("countryCode", "US") if r.status_code == 200 else "US"
        except: return "US"

    # ==========================
    # 🔎 ZONA DE BÚSQUEDA
    # ==========================
    def search(self, query, limit=10):
        """Busca tracks y retorna JSON limpio."""
        url = "https://api.tidal.com/v1/search"
        params = {"query": query, "limit": limit, "offset": 0, "types": "TRACKS", "countryCode": self.country_code}
        try:
            r = requests.get(url, headers=self.headers, params=params)
            items = r.json().get("tracks", {}).get("items", [])
            resultados = []
            for t in items:
                lista = t.get("artists", [])
                nombres = [a.get("name") for a in lista if a.get("name")]
                artista = ", ".join(nombres) if nombres else "Desconocido"
                
                resultados.append({
                    "id": t.get("id"),
                    "titulo": t.get("title"),
                    "artista": artista,
                    "album": t.get("album", {}).get("title"),
                    "calidad": t.get("audioQuality"),
                    "explicit": t.get("explicit"),
                    "cover": t.get("album", {}).get("cover")
                })
            return json.dumps(resultados, indent=4, ensure_ascii=False)
        except Exception as e: return json.dumps({"error": str(e)})

    # ==========================
    # 💾 MÉTODOS PÚBLICOS (FACADE)
    # ==========================
    
    def save_mp3(self, track_id, name="{Artist} - {Title}", output_folder=None):
        return self._motor_descarga(track_id, "MP3", name, output_folder)

    def save_flac(self, track_id, name="{Artist} - {Title}", output_folder=None):
        return self._motor_descarga(track_id, "FLAC", name, output_folder)

    def save_hires(self, track_id, name="{Artist} - {Title}", output_folder=None):
        return self._motor_descarga(track_id, "HIRES", name, output_folder)

    # ==========================
    # ⚙️ MOTOR PRIVADO
    # ==========================
    def _sanitizar(self, nombre):
        return re.sub(r'[\\/*?:"<>|]', "", nombre)

    def _preparar_nombre(self, patron, meta, calidad):
        ext = ".mp3" if calidad == "MP3" else ".flac"
        datos = {
            "Title": meta['title'], "Artist": meta['artist'],
            "Album": meta['album'], "Year": meta['year'], "Track": meta['track_num']
        }
        try:
            nombre_base = patron.format(**datos)
        except (KeyError, IndexError, ValueError):
            nombre_base = patron

        return f"{self._sanitizar(nombre_base)}{ext}"

    def _obtener_urls(self, track_id, calidad):
        api_q = self.CALIDADES_MAP.get(calidad, "LOSSLESS")
        url = f"https://api.tidal.com/v1/tracks/{track_id}/playbackinfo?audioquality={api_q}&playbackmode=STREAM&assetpresentation=FULL&countryCode={self.country_code}"
        try:
            d = requests.get(url, headers=self.headers).json()
            if "manifest" not in d: return None
            xml = base64.b64decode(d["manifest"]).decode('utf-8')
            init = re.search(r'initialization="([^"]+)"', xml)
            media = re.search(r'media="([^"]+)"', xml)
            if init and media:
                return init.group(1).replace("&amp;", "&"), media.group(1).replace("&amp;", "&")
        except: pass
        return None

    def _motor_descarga(self, track_id, calidad, nombre_formato, output_folder):
        # Configuramos la barra de progreso
        progress = Progress(
            SpinnerColumn(), TextColumn("[bold blue]{task.description}"),
            BarColumn(), TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
            TimeElapsedColumn()
        )
        
        with progress:
            task = progress.add_task("Iniciando...", total=100)
            
            # 1. Metadata
            progress.update(task, description=f"🔍 Analizando ID {track_id}...", advance=5)
            url_meta = f"https://api.tidal.com/v1/tracks/{track_id}"
            try:
                d = requests.get(url_meta, headers=self.headers, params={"countryCode": self.country_code}).json()
                cov = d.get("album", {}).get("cover")
                meta = {
                    "title": d.get("title"), "artist": d.get("artist", {}).get("name"),
                    "album": d.get("album", {}).get("title"), "year": d.get("streamStartDate", "")[:4],
                    "track_num": str(d.get("trackNumber")).zfill(2), "total_tracks": str(d.get("volumeNumber")),
                    "copyright": d.get("copyright"),
                    "cover_url": f"https://resources.tidal.com/images/{cov.replace('-', '/')}/1280x1280.jpg" if cov else None
                }
            except: 
                console.print("[red]❌ Error obteniendo metadatos.[/]")
                return

            # 2. Nombre y Ruta
            nombre_final = self._preparar_nombre(nombre_formato, meta, calidad)
            if output_folder:
                if not os.path.exists(output_folder): os.makedirs(output_folder)
                ruta_final = os.path.join(output_folder, nombre_final)
            else:
                ruta_final = nombre_final

            # 3. URLs
            progress.update(task, description="🔗 Obteniendo stream...", advance=10)
            urls = self._obtener_urls(track_id, calidad)
            if not urls:
                console.print("[red]❌ Error: Calidad no disponible o token inválido.[/]")
                return
            init_url, chunk_url = urls

            # 4. Descarga Multihilo
            temp_dir = f"temp_{track_id}"
            if os.path.exists(temp_dir): shutil.rmtree(temp_dir)
            os.makedirs(temp_dir)

            progress.update(task, description=f"⬇️ Descargando {calidad}...", total=300)
            segmentos = []
            def _dl(n):
                u = init_url if n==0 else chunk_url.replace("$Number$", str(n))
                try:
                    r = requests.get(u, stream=True, timeout=10)
                    if r.status_code == 200:
                        with open(os.path.join(temp_dir, f"{n}.mp4"), 'wb') as f: f.write(r.content)
                        return n, True
                except: pass
                return n, False

            with ThreadPoolExecutor(max_workers=20) as ex:
                futures = {ex.submit(_dl, i): i for i in range(300)}
                for f in as_completed(futures):
                    n, ok = f.result()
                    if ok: 
                        segmentos.append(n)
                        progress.update(task, advance=1)
            
            if 0 not in segmentos:
                shutil.rmtree(temp_dir)
                return

            # 5. Unión
            progress.update(task, description="🧩 Ensamblando...", total=None)
            segmentos.sort()
            ultimo = 0
            for i in range(len(segmentos)):
                if segmentos[i] == i: ultimo = i
                else: break
            
            raw = os.path.join(temp_dir, "raw.mp4")
            with open(raw, 'wb') as outfile:
                for i in range(ultimo + 1):
                    with open(os.path.join(temp_dir, f"{i}.mp4"), 'rb') as infile: outfile.write(infile.read())

            # 6. Conversión FFmpeg
            progress.update(task, description="⚙️ Convirtiendo...", total=100)
            cmd = ['ffmpeg', '-y', '-i', raw, '-map_metadata', '-1', '-loglevel', 'error']
            if calidad == "MP3": cmd.extend(['-codec:a', 'libmp3lame', '-b:a', '320k', ruta_final])
            else: cmd.extend(['-c', 'copy', ruta_final])
            
            try: subprocess.run(cmd, check=True)
            except: shutil.move(raw, ruta_final)

            # 7. Tags
            progress.update(task, description="🎨 Tagging...", advance=100)
            self._aplicar_tags(ruta_final, meta, calidad)
            shutil.rmtree(temp_dir)

        # === ZONA FINAL (TEXTO PLANO) ===
        console.clear()
        # Solo imprimimos esto:
        console.print(f"[bold green]✅ Descarga Exitosa:[/] {nombre_final}")

    def _aplicar_tags(self, filepath, meta, calidad):
        img = None
        if meta['cover_url']:
            try: img = requests.get(meta['cover_url']).content
            except: pass
        
        try:
            if calidad in ["FLAC", "HIRES"]:
                a = FLAC(filepath)
                a["title"] = meta['title']
                a["artist"] = meta['artist']
                a["album"] = meta['album']
                a["date"] = meta['year']
                a["tracknumber"] = meta['track_num']
                a["copyright"] = meta['copyright']
                if img:
                    p = Picture()
                    p.type = 3; p.mime = "image/jpeg"; p.desc = "Cover"; p.data = img
                    a.add_picture(p)
                a.save()
            elif calidad == "MP3":
                try: a = ID3(filepath)
                except: a = ID3()
                a.add(TIT2(encoding=3, text=meta['title']))
                a.add(TPE1(encoding=3, text=meta['artist']))
                a.add(TALB(encoding=3, text=meta['album']))
                a.add(TYER(encoding=3, text=meta['year']))
                a.add(TRCK(encoding=3, text=f"{meta['track_num']}/{meta['total_tracks']}"))
                a.add(TCON(encoding=3, text=meta['copyright']))
                if img: a.add(APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=img))
                a.save(filepath, v2_version=3)
        except: pass