import type { Song, SongType } from '../types/song';

export interface ScannedTrack {
  id: string;
  name: string;
  artist: string;
  album: string;
  duration: number;
  file: File;
  fileHandle?: FileSystemFileHandle;
  url?: string;
}

const AUDIO_EXTS = new Set(['.mp3', '.flac', '.wav', '.ogg', '.aac', '.m4a', '.wma', '.ape', '.opus']);

function isAudioFile(name: string): boolean {
  const dot = name.lastIndexOf('.');
  if (dot === -1) return false;
  return AUDIO_EXTS.has(name.slice(dot).toLowerCase());
}

function parseFileName(name: string): { title: string; artist: string } {
  const dot = name.lastIndexOf('.');
  const base = dot !== -1 ? name.slice(0, dot) : name;
  const separators = [' - ', ' – ', ' — ', ' -', '-', ' / ', '/'];
  for (const sep of separators) {
    const idx = base.indexOf(sep);
    if (idx > 0 && idx < base.length - sep.length) {
      return { artist: base.slice(0, idx).trim(), title: base.slice(idx + sep.length).trim() };
    }
  }
  return { title: base, artist: '' };
}

function genId(): string {
  return 'local-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
}

async function getDuration(file: File): Promise<number> {
  return new Promise((resolve) => {
    const url = URL.createObjectURL(file);
    const audio = new Audio();
    audio.preload = 'metadata';
    const timer = setTimeout(() => { audio.src = ''; URL.revokeObjectURL(url); resolve(0); }, 5000);
    audio.onloadedmetadata = () => {
      clearTimeout(timer);
      const dur = Math.round(audio.duration * 1000);
      audio.src = '';
      URL.revokeObjectURL(url);
      resolve(dur);
    };
    audio.onerror = () => { clearTimeout(timer); audio.src = ''; URL.revokeObjectURL(url); resolve(0); };
    audio.src = url;
    audio.load();
  });
}

export async function scanFile(file: File, fileHandle?: FileSystemFileHandle): Promise<ScannedTrack | null> {
  if (!isAudioFile(file.name)) return null;
  const parsed = parseFileName(file.name);
  const duration = await getDuration(file);
  return {
    id: genId(),
    name: parsed.title || file.name,
    artist: parsed.artist || '未知歌手',
    album: '',
    duration,
    file,
    fileHandle,
  };
}

export async function scanDirectory(dirHandle: FileSystemDirectoryHandle, onProgress?: (name: string) => void): Promise<ScannedTrack[]> {
  const results: ScannedTrack[] = [];
  const entries: [string, FileSystemHandle][] = [];
  for await (const entry of (dirHandle as any).entries()) {
    entries.push(entry);
  }
  for (const [name, handle] of entries) {
    if (onProgress) onProgress(name);
    if (handle.kind === 'file') {
      const fileHandle = handle as FileSystemFileHandle;
      const file = await fileHandle.getFile();
      const track = await scanFile(file, fileHandle);
      if (track) results.push(track);
    } else if (handle.kind === 'directory') {
      const sub = await scanDirectory(handle as FileSystemDirectoryHandle, onProgress);
      results.push(...sub);
    }
  }
  return results;
}

export function trackToSong(track: ScannedTrack): Song {
  return {
    id: track.id,
    name: track.name,
    artist: track.artist,
    picUrl: '',
    url: track.url || '',
    duration: formatDuration(track.duration),
    source: 'local',
    type: 'Local' as SongType,
    albumName: track.album || null,
    quality: 'standard',
  };
}

export function formatDuration(ms: number): string {
  if (!ms || ms <= 0) return '0:00';
  const totalSec = Math.floor(ms / 1000);
  const min = Math.floor(totalSec / 60);
  const sec = totalSec % 60;
  return `${min}:${sec.toString().padStart(2, '0')}`;
}

const DB_NAME = 'qz-local-music';
const DB_VERSION = 1;
const STORE_NAME = 'tracks';

function openDB(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        db.createObjectStore(STORE_NAME, { keyPath: 'id' });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

export async function saveTracks(tracks: ScannedTrack[]): Promise<void> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite');
    const store = tx.objectStore(STORE_NAME);
    for (const t of tracks) {
      store.put({ id: t.id, name: t.name, artist: t.artist, album: t.album, duration: t.duration });
    }
    tx.oncomplete = () => { db.close(); resolve(); };
    tx.onerror = () => { db.close(); reject(tx.error); };
  });
}

export async function loadTracks(): Promise<ScannedTrack[]> {
  try {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readonly');
      const store = tx.objectStore(STORE_NAME);
      const req = store.getAll();
      req.onsuccess = () => { db.close(); resolve(req.result || []); };
      req.onerror = () => { db.close(); reject(req.error); };
    });
  } catch {
    return [];
  }
}

export async function clearSavedTracks(): Promise<void> {
  try {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readwrite');
      tx.objectStore(STORE_NAME).clear();
      tx.oncomplete = () => { db.close(); resolve(); };
      tx.onerror = () => { db.close(); reject(tx.error); };
    });
  } catch { /* noop */ }
}

export function supportsDirectoryPicker(): boolean {
  return typeof (window as any).showDirectoryPicker === 'function';
}

/**
 * Extract embedded lyrics from ID3v2 tags (USLT frame) in audio files.
 * Returns the lyrics text as a string, or null if not found.
 */
export async function extractEmbeddedLyrics(file: File): Promise<string | null> {
  const chunk = await file.slice(0, 1024 * 1024).arrayBuffer();
  const bytes = new Uint8Array(chunk);
  // "ID3"
  if (bytes[0] !== 0x49 || bytes[1] !== 0x44 || bytes[2] !== 0x33) return null;
  const tagSize = readSynchSafeInt(bytes, 6);
  const id3End = Math.min(10 + tagSize, bytes.length);
  let pos = 10;
  while (pos + 10 <= id3End) {
    const frameId = String.fromCharCode(bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3]);
    const frameSize = readSynchSafeInt(bytes, pos + 4);
    pos += 10;
    if (frameSize === 0) break;
    if (pos + frameSize > id3End) break;
    if (frameId === 'USLT') {
      // encoding byte
      const encoding = bytes[pos];
      // skip language (3 bytes) + null-terminated content descriptor
      let offset = pos + 4;
      while (offset < pos + frameSize && bytes[offset] !== 0) offset++;
      offset++;
      const lyricBytes = bytes.slice(offset, pos + frameSize);
      if (encoding === 0) return decodeText(lyricBytes, 'latin1');
      if (encoding === 1) return decodeText(lyricBytes, 'utf-16');
      if (encoding === 2) return decodeText(lyricBytes, 'utf-16le');
      if (encoding === 3) return decodeText(lyricBytes, 'utf-8');
      return decodeText(lyricBytes, 'utf-8');
    }
    if (frameId === 'SYLT') {
      // SYLT frame - synchronized lyrics, extract text parts
      const encoding = bytes[pos];
      const format = bytes[pos + 4];
      const contentType = bytes[pos + 5];
      if (contentType !== 1) { pos += frameSize; continue; }
      let offset = pos + 6;
      while (offset < pos + frameSize && bytes[offset] !== 0) offset++;
      offset++;
      const lyricParts: string[] = [];
      while (offset + 8 <= pos + frameSize) {
        if (format === 1 || format === 2) {
          let end = offset;
          while (end < pos + frameSize && bytes[end] !== 0) end++;
          lyricParts.push(decodeText(bytes.slice(offset, end), encoding === 0 ? 'latin1' : 'utf-16') || '');
          offset = end + 1;
          offset += 4;
        } else {
          break;
        }
      }
      const text = lyricParts.join('\n');
      if (text.trim()) return text;
    }
    pos += frameSize;
  }
  return null;
}

function readSynchSafeInt(data: Uint8Array, offset: number): number {
  return ((data[offset] & 0x7f) << 21) | ((data[offset+1] & 0x7f) << 14)
       | ((data[offset+2] & 0x7f) << 7) | (data[offset+3] & 0x7f);
}

function decodeText(data: Uint8Array, encoding: string): string {
  if (encoding === 'utf-16' || encoding === 'utf-16le') {
    if (data.length < 4) return '';

    if (data[0] === 0 && data[1] === 0) return '';
    let str = '';
    for (let i = 0; i + 1 < data.length; i += 2) {
      const code = (data[i+1] << 8) | data[i];
      if (code === 0) break;
      str += String.fromCodePoint(code);
    }
    return str;
  }
  let str = '';
  for (let i = 0; i < data.length; i++) {
    if (data[i] === 0) break;
    str += String.fromCharCode(data[i]);
  }
  return str;
}
