from flask import Flask, jsonify, request
from flask_cors import CORS
from datetime import datetime
from models import init_db, SessionLocal, Profile, Song, Plan, PlanSong, ProfileSong
try:
    from dotenv import load_dotenv
    load_dotenv()  # Loads variables from a .env file if present
except ImportError:
    # dotenv not installed yet; proceed without loading .env
    pass

import os
import json

app = Flask(__name__)
# Enhanced CORS with wildcard support for all origins
CORS(app, resources={
    r"/api/*": {
        "origins": "*",
        "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        "allow_headers": ["Content-Type", "Authorization"]
    }
})
init_db()
FLASK_PORT = int(os.environ.get('FLASK_PORT', '5000'))

def get_db():
    """Create a new database session with automatic cleanup"""
    db = SessionLocal()
    try:
        return db
    except Exception:
        db.close()
        raise

@app.teardown_appcontext
def cleanup_session(exception=None):
    """Cleanup scoped database sessions after each request to prevent connection pool exhaustion"""
    SessionLocal.remove()

def extract_text_from_file(file_storage):
    """Extract text from an uploaded file (docx, pdf, image, txt)."""
    filename = file_storage.filename or ''
    lower = filename.lower()
    data = file_storage.read()
    # Reset stream position if needed for certain libraries
    import io
    stream = io.BytesIO(data)
    text = ''
    try:
        if lower.endswith('.txt'):
            text = data.decode(errors='ignore')
        elif lower.endswith('.docx'):
            try:
                import docx
                doc = docx.Document(stream)
                text = '\n'.join(p.text for p in doc.paragraphs)
            except Exception:
                text = ''
        elif lower.endswith('.pdf'):
            try:
                import PyPDF2
                reader = PyPDF2.PdfReader(stream)
                parts = []
                for page in reader.pages[:20]:  # safety limit
                    try:
                        parts.append(page.extract_text() or '')
                    except Exception:
                        continue
                text = '\n'.join(parts)
                # OCR fallback if minimal text extracted (likely scanned PDF)
                if len(text.strip()) < 50:
                    try:
                        from pdf2image import convert_from_bytes
                        from PIL import Image
                        import pytesseract
                        images = convert_from_bytes(data, first_page=1, last_page=min(10, len(reader.pages)))
                        ocr_parts = []
                        for img in images:
                            ocr_parts.append(pytesseract.image_to_string(img))
                        ocr_text = '\n'.join(ocr_parts)
                        if len(ocr_text.strip()) > len(text.strip()):
                            text = ocr_text
                    except Exception:
                        pass  # Fall back to original text layer extraction
            except Exception:
                text = ''
        elif lower.endswith(('.png','.jpg','.jpeg','.tif','.tiff')):
            try:
                from PIL import Image
                import pytesseract
                img = Image.open(stream)
                text = pytesseract.image_to_string(img)
            except Exception:
                text = ''
        else:
            # Attempt generic decode
            try:
                text = data.decode(errors='ignore')
            except Exception:
                text = ''
    except Exception:
        text = ''
    # Basic cleanup
    cleaned = '\n'.join(line.rstrip() for line in (text or '').splitlines())
    return cleaned.strip()

# Admin restore from data.json (profiles and songs)
@app.route('/api/admin/restore', methods=['POST'])
def admin_restore():
    # Determine data.json location: prefer backend/data.json, fallback to project root
    backend_path = os.path.join(os.path.dirname(__file__), 'data.json')
    root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'data.json'))
    source_path = backend_path if os.path.exists(backend_path) else root_path if os.path.exists(root_path) else None
    if not source_path:
        return jsonify({ 'ok': False, 'error': 'data.json not found in backend/ or project root' }), 404

    try:
        with open(source_path, 'r', encoding='utf-8') as f:
            payload = json.load(f)
    except Exception as e:
        return jsonify({ 'ok': False, 'error': f'Failed to read data.json: {str(e)}' }), 400

    db = SessionLocal()
    created = { 'profiles': 0, 'songs': 0 }
    updated = { 'profiles': 0, 'songs': 0 }

    try:
        import uuid
        # Restore profiles
        profiles = payload.get('profiles', [])
        for p in profiles:
            name = (p.get('name') or f"{(p.get('first_name') or '').strip()} {(p.get('last_name') or '').strip()}" ).strip()
            if not name:
                continue
            existing = db.query(Profile).filter(Profile.name == name).first()
            if existing:
                changed = False
                for key in ['email','contact_number','default_key','notes']:
                    if p.get(key) and getattr(existing, key, None) != p.get(key):
                        setattr(existing, key, p.get(key))
                        changed = True
                if changed:
                    updated['profiles'] += 1
            else:
                profile_id = str(p.get('id')) if p.get('id') else str(uuid.uuid4())
                new_p = Profile(
                    id=profile_id,
                    name=name,
                    email=p.get('email') or '',
                    contact_number=p.get('contact_number') or '',
                    default_key=p.get('default_key') or 'C',
                    notes=p.get('notes') or ''
                )
                db.add(new_p)
                created['profiles'] += 1

        # Restore songs
        songs = payload.get('songs', [])
        for s in songs:
            title = (s.get('title') or '').strip()
            if not title:
                continue
            existing = db.query(Song).filter(Song.title == title).first()
            lyrics = s.get('lyrics') or s.get('content') or ''
            chords = s.get('chords') or ''
            singer = s.get('singer') or s.get('artist') or ''
            artist = s.get('artist') or ''
            band = s.get('band') or ''
            if existing:
                changed = False
                if lyrics and (existing.lyrics or '') != lyrics:
                    existing.lyrics = lyrics
                    changed = True
                if chords and (existing.chords or '') != chords:
                    existing.chords = chords
                    changed = True
                if singer and (existing.singer or '') != singer:
                    existing.singer = singer
                    changed = True
                if artist and (existing.artist or '') != artist:
                    existing.artist = artist
                    changed = True
                if band and (existing.band or '') != band:
                    existing.band = band
                    changed = True
                if changed:
                    updated['songs'] += 1
            else:
                song_id = str(s.get('id')) if s.get('id') else str(uuid.uuid4())
                new_s = Song(id=song_id, title=title, artist=artist, band=band, singer=singer, lyrics=lyrics, chords=chords)
                db.add(new_s)
                created['songs'] += 1

        db.commit()
        return jsonify({ 'ok': True, 'profiles_created': created['profiles'], 'profiles_updated': updated['profiles'], 'songs_created': created['songs'], 'songs_updated': updated['songs'], 'source': source_path })
    except Exception as e:
        db.rollback()
        return jsonify({ 'ok': False, 'error': str(e) }), 500
    finally:
        db.close()

@app.route('/api/upload_lyric', methods=['POST'])
def upload_lyric():
    """Accept a lyric file and return extracted text. Does NOT create a Song automatically."""
    if 'file' not in request.files:
        return jsonify({'error':'file_missing'}), 400
    f = request.files['file']
    if not f.filename:
        return jsonify({'error':'empty_filename'}), 400
    # Optional metadata fields from form
    title = request.form.get('title') or f.filename.rsplit('.',1)[0]
    artist = request.form.get('artist') or ''
    band = request.form.get('band') or ''
    extracted = extract_text_from_file(f)
    if not extracted:
        return jsonify({'error':'extraction_failed','title':title}), 422
    # Return without saving; front-end will present in modal for editing & saving
    sample = extracted[:150] + ('...' if len(extracted) > 150 else '')
    return jsonify({
        'status':'ok',
        'title': title,
        'artist': artist,
        'band': band,
        'lyrics': extracted,
        'preview': sample,
        'length': len(extracted)
    })

@app.route('/api/providers')
def providers():
    """Report which external provider tokens are configured (OpenAI removed)."""
    return jsonify({
        'local_db': True,
        'chartlyrics': True,
        'lifeway_link': True
    })

@app.route('/')
def index():
    return jsonify({'message': 'House of Prayer Song Lyrics API (Flask)', 'port': FLASK_PORT})

@app.route('/api/health')
def health():
    return jsonify({'status': 'ok', 'ts': datetime.utcnow().isoformat()})

# Profile Selection Management
@app.route('/api/profile-selection/clear', methods=['POST', 'OPTIONS'])
def clear_profile_selection():
    """Clear profile selection (frontend manages this in localStorage, but endpoint needed for API compatibility)"""
    if request.method == 'OPTIONS':
        return '', 204
    return jsonify({'status': 'ok', 'message': 'Profile selection cleared'})

# Profiles CRUD
@app.route('/api/profiles', methods=['GET','POST'])
def profiles():
    db = get_db()
    if request.method == 'GET':
        items = db.query(Profile).all()
        result = jsonify([{'id':p.id,'name':p.name,'email':p.email,'contact_number':p.contact_number,'default_key':p.default_key,'notes':p.notes} for p in items])
        db.close()
        return result
    import uuid
    data = request.get_json() or {}
    profile_id = data.get('id') or str(uuid.uuid4())
    p = Profile(id=profile_id, name=data.get('name'), email=data.get('email') or '', contact_number=data.get('contact_number') or '', default_key=data.get('default_key') or 'C', notes=data.get('notes') or '')
    db.add(p); db.commit();
    result = jsonify({'id':p.id})
    db.close()
    return result

@app.route('/api/profiles/<pid>', methods=['PUT','DELETE'])
def profile_item(pid):
    db = get_db(); p = db.query(Profile).get(pid)
    if not p: return jsonify({'error':'not_found'}),404
    if request.method == 'PUT':
        d = request.get_json() or {}
        p.name = d.get('name', p.name)
        p.email = d.get('email', p.email)
        p.contact_number = d.get('contact_number', p.contact_number)
        p.default_key = d.get('default_key', p.default_key)
        p.notes = d.get('notes', p.notes)
        db.commit(); return jsonify({'status':'ok'})
    db.delete(p); db.commit(); return jsonify({'status':'deleted'})

# Songs CRUD + search (local)
@app.route('/api/songs', methods=['GET','POST'])
def songs():
    db = get_db()
    if request.method == 'GET':
        q = request.args.get('q','').lower()
        items = db.query(Song).all()
        def match(s):
            return (q in (s.title or '').lower() or q in (s.artist or '').lower() or q in (s.band or '').lower() or q in (s.singer or '').lower()) if q else True
        # Include lyrics preview (first 200 chars) for Database component
        return jsonify([{
            'id':s.id,
            'title':s.title,
            'artist':s.artist,
            'band':s.band,
            'singer':s.singer,
            'lyrics':(s.lyrics or '')[:200] if s.lyrics else '',
            'chords':(s.chords or '')[:100] if s.chords else ''
        } for s in items if match(s)])
    import uuid
    d = request.get_json() or {}
    song_id = d.get('id') or str(uuid.uuid4())
    s = Song(id=song_id, title=d.get('title') or 'Untitled', artist=d.get('artist') or '', band=d.get('band') or '', singer=d.get('singer') or '', lyrics=d.get('lyrics') or '', chords=d.get('chords') or '')
    db.add(s); db.commit(); return jsonify({'id':s.id})

@app.route('/api/songs/<sid>', methods=['GET','PUT','DELETE'])
def song_item(sid):
    db = get_db(); s = db.query(Song).get(sid)
    if not s: return jsonify({'error':'not_found'}),404
    if request.method == 'GET':
        return jsonify({'id':s.id,'title':s.title,'artist':s.artist,'band':s.band,'singer':s.singer,'lyrics':s.lyrics,'chords':s.chords})
    if request.method == 'PUT':
        d = request.get_json() or {}
        s.title = d.get('title', s.title)
        s.artist = d.get('artist', s.artist)
        s.band = d.get('band', s.band)
        s.singer = d.get('singer', s.singer)
        s.lyrics = d.get('lyrics', s.lyrics)
        s.chords = d.get('chords', s.chords)
        db.commit(); return jsonify({'status':'ok'})
    db.delete(s); db.commit(); return jsonify({'status':'deleted'})

# Planning (date-based)
@app.route('/api/plans', methods=['GET','POST'])
def plans():
    db = get_db()
    if request.method == 'GET':
        items = db.query(Plan).all()
        return jsonify([{'id':p.id,'date':p.date.isoformat(),'profile_id':p.profile_id,'memo':p.memo} for p in items])
    d = request.get_json() or {}
    date_str = d.get('date'); date = datetime.fromisoformat(date_str).date() if date_str else datetime.now().date()
    plan = Plan(date=date, profile_id=d.get('profile_id'), memo=d.get('memo') or '')
    db.add(plan); db.commit(); return jsonify({'id':plan.id})

@app.route('/api/plans/<int:pid>/songs', methods=['GET','POST'])
def plan_songs(pid):
    db = get_db(); plan = db.query(Plan).get(pid)
    if not plan: return jsonify({'error':'plan_not_found'}),404
    if request.method == 'GET':
        links = db.query(PlanSong).filter(PlanSong.plan_id==pid).order_by(PlanSong.order_index).all()
        return jsonify([{'id':l.id,'song_id':l.song_id,'order_index':l.order_index} for l in links])
    d = request.get_json() or {}
    link = PlanSong(plan_id=pid, song_id=d.get('song_id'), order_index=d.get('order_index') or 0)
    db.add(link); db.commit(); return jsonify({'id':link.id})

# Profile Songs endpoints
@app.route('/api/profiles/<pid>/songs', methods=['GET','POST'])
def profile_songs(pid):
    db = get_db()
    try:
        profile = db.query(Profile).get(pid)
        if not profile:
            return jsonify({'error':'profile_not_found'}),404
        if request.method == 'GET':
            links = db.query(ProfileSong).filter(ProfileSong.profile_id==pid).all()
            return jsonify([{'id':l.id,'profile_id':l.profile_id,'song_id':l.song_id,'song_key':l.song_key} for l in links])
        d = request.get_json() or {}
        # Check if association already exists
        existing = db.query(ProfileSong).filter(ProfileSong.profile_id==pid, ProfileSong.song_id==d.get('song_id')).first()
        if existing:
            return jsonify({'id':existing.id, 'exists':True})
        link = ProfileSong(profile_id=pid, song_id=d.get('song_id'), song_key=d.get('song_key') or 'C')
        db.add(link); db.commit()
        return jsonify({'id':link.id})
    finally:
        db.close()

@app.route('/api/profiles/<pid>/songs/<sid>', methods=['GET','PUT','DELETE'])
def profile_song_item(pid, sid):
    db = get_db()
    try:
        link = db.query(ProfileSong).filter(ProfileSong.profile_id==pid, ProfileSong.song_id==sid).first()
        if not link:
            return jsonify({'error':'not_found'}),404
        if request.method == 'GET':
            return jsonify({'id':link.id,'profile_id':link.profile_id,'song_id':link.song_id,'song_key':link.song_key})
        if request.method == 'PUT':
            d = request.get_json() or {}
            link.song_key = d.get('song_key', link.song_key)
            db.commit()
            return jsonify({'status':'ok'})
        db.delete(link); db.commit()
        return jsonify({'status':'deleted'})
    finally:
        db.close()

# External search using ChartLyrics API + Local Database
@app.route('/api/search_external')
def search_external():
    import urllib.request
    import urllib.parse
    import xml.etree.ElementTree as ET
    
    q = request.args.get('q', '')
    filter_type = request.args.get('filter', 'all')
    
    if not q:
        return jsonify({'query': q, 'results': []})
    
    results = []
    
    # First, search local database
    db = get_db()
    local_songs = db.query(Song).all()
    
    def match_local(s):
        q_lower = q.lower()
        if filter_type == 'title':
            return q_lower in (s.title or '').lower()
        elif filter_type == 'artist':
            return q_lower in (s.artist or '').lower()
        elif filter_type == 'band':
            return q_lower in (s.band or '').lower()
        else:  # all
            return (q_lower in (s.title or '').lower() or 
                    q_lower in (s.artist or '').lower() or 
                    q_lower in (s.band or '').lower())
    
    for s in local_songs:
        if match_local(s):
            snippet = (s.lyrics or '')[:150] + '...' if len(s.lyrics or '') > 150 else (s.lyrics or '')
            results.append({
                'title': s.title,
                'artist': s.artist,
                'band': s.band,
                'snippet': snippet,
                'source': 'local_db'
            })
    
    # Then search ChartLyrics (free API)
    try:
        # ChartLyrics search - expects artist and song
        # We'll try different combinations based on filter
        search_terms = []
        
        if filter_type == 'artist':
            search_terms.append({'artist': q, 'song': ''})
        elif filter_type == 'title':
            search_terms.append({'artist': '', 'song': q})
        else:
            # For 'all' and 'band', try both
            search_terms.append({'artist': q, 'song': ''})
            search_terms.append({'artist': '', 'song': q})
        
        for term in search_terms:
            try:
                artist_encoded = urllib.parse.quote(term['artist'])
                song_encoded = urllib.parse.quote(term['song'])
                url = f'http://api.chartlyrics.com/apiv1.asmx/SearchLyric?artist={artist_encoded}&song={song_encoded}'
                
                with urllib.request.urlopen(url, timeout=5) as response:
                    xml_data = response.read()
                    root = ET.fromstring(xml_data)
                    
                    # Parse XML results (ChartLyrics returns XML)
                    namespace = {'cl': 'http://api.chartlyrics.com/'}
                    for item in root.findall('.//cl:SearchLyricResult', namespace):
                        title = item.find('cl:Song', namespace)
                        artist = item.find('cl:Artist', namespace)
                        lyric_id = item.find('cl:LyricId', namespace)
                        
                        if title is not None and artist is not None:
                            # Get actual lyrics
                            snippet = "Preview available - click to view full lyrics"
                            
                            results.append({
                                'title': title.text or 'Unknown',
                                'artist': artist.text or 'Unknown',
                                'band': artist.text or 'Unknown',
                                'snippet': snippet,
                                'source': 'chartlyrics',
                                'lyric_id': lyric_id.text if lyric_id is not None else None
                            })
                        
                        # Limit to 5 results per search
                        if len([r for r in results if r.get('source') == 'chartlyrics']) >= 5:
                            break
            except:
                continue
                
    except Exception as e:
        # If external API fails, we still have local results
        pass
    
    # (Genius integration removed per user request)

    # Remove duplicates and limit total results
    seen = set()
    unique_results = []
    for r in results:
        key = (r['title'].lower(), r['artist'].lower())
        if key not in seen:
            seen.add(key)
            unique_results.append(r)
            if len(unique_results) >= 10:
                break
    
    # Add Lifeway Worship search link as an additional source entry
    try:
        import urllib.parse as _urlparse
        lifeway_url = f"https://worship.lifeway.com/findAndBuy/home?findMappable=false&searchString={_urlparse.quote(q)}"
        unique_results.append({
            'title': f'Search on Lifeway: {q}',
            'artist': '',
            'band': 'Lifeway Worship',
            'snippet': 'Open Lifeway Worship results for this query',
            'source': 'lifeway',
            'url': lifeway_url
        })
    except Exception:
        pass
    
    return jsonify({
        'query': q,
        'filter': filter_type,
        'results': unique_results,
        'sources': 'Local DB + ChartLyrics (+ Lifeway link)'
    })

@app.route('/api/export/<int:plan_id>')
def export_plan(plan_id):
    """Export a plan's songs as a formatted text document."""
    db = get_db()
    plan = db.query(Plan).get(plan_id)
    if not plan:
        return jsonify({'error':'plan_not_found'}), 404
    
    profile = db.query(Profile).get(plan.profile_id) if plan.profile_id else None
    plan_songs = db.query(PlanSong).filter(PlanSong.plan_id==plan_id).order_by(PlanSong.order_index).all()
    
    lines = []
    lines.append('=' * 60)
    lines.append(f'WORSHIP SETLIST: {plan.date.strftime("%B %d, %Y")}')
    if profile:
        lines.append(f'Profile: {profile.name} (Key: {profile.default_key})')
    if plan.memo:
        lines.append(f'Notes: {plan.memo}')
    lines.append('=' * 60)
    lines.append('')
    
    for idx, ps in enumerate(plan_songs, 1):
        song = db.query(Song).get(ps.song_id)
        if not song:
            continue
        lines.append(f'{idx}. {song.title}')
        lines.append(f'   Artist: {song.artist or "Unknown"} | Band: {song.band or "Unknown"}')
        lines.append('')
        if song.lyrics:
            lines.append('   LYRICS:')
            for line in song.lyrics.splitlines():
                lines.append(f'   {line}')
            lines.append('')
        if song.chords:
            lines.append('   CHORDS:')
            for line in song.chords.splitlines():
                lines.append(f'   {line}')
            lines.append('')
        lines.append('-' * 60)
        lines.append('')
    
    content = '\n'.join(lines)
    response = app.make_response(content)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    response.headers['Content-Disposition'] = f'attachment; filename="setlist_{plan.date.isoformat()}.txt"'
    return response

if __name__ == '__main__':
    # Bind to all interfaces for optional external testing on alternate port
    app.run(host='0.0.0.0', port=FLASK_PORT, debug=True)
