import os
import logging
import importlib.util
import json
from flask import Flask, session, redirect, url_for, request
from flask_login import LoginManager, current_user
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
from werkzeug.middleware.proxy_fix import ProxyFix
from importlib import import_module
from musicround.config import Config
from musicround.version import VERSION_INFO, get_version_str
from datetime import datetime
from musicround.helpers.auth_helpers import oauth # Import the oauth object

# Initialize SQLAlchemy
db = SQLAlchemy()
login_manager = LoginManager()
csrf = CSRFProtect()

# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def run_migrations():
    """
    Run all migration scripts in the migrations directory
    """
    logger.info("Running database migrations...")
    
    # Path to migrations directory
    migrations_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'migrations')
    if not os.path.isdir(migrations_dir):
        logger.warning(f"Migrations directory not found at {migrations_dir}")
        return
    
    # Get all Python files in the migrations directory
    migration_files = [f for f in os.listdir(migrations_dir) 
                      if f.endswith('.py') and not f.startswith('__')]
    
    if not migration_files:
        logger.info("No migration scripts found")
        return
    
    # Run each migration script
    migration_errors = False
    
    for migration_file in sorted(migration_files):
        try:
            logger.info(f"Loading migration: {migration_file}")
            file_path = os.path.join(migrations_dir, migration_file)
            
            # Load the module dynamically
            spec = importlib.util.spec_from_file_location(
                f"migrations.{migration_file[:-3]}", file_path)
            if spec and spec.loader:
                migration_module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(migration_module)
                
                # Check if the module has a run_migration function
                if hasattr(migration_module, "run_migration"):
                    logger.info(f"Executing migration: {migration_file}")
                    result = migration_module.run_migration()
                    
                    # Handle the three possible return values:
                    # True: changes were made successfully
                    # None: no changes needed (already up to date)
                    # False: errors occurred
                    if result is True:
                        logger.info(f"Migration {migration_file} completed successfully")
                    elif result is None:
                        logger.info(f"Migration {migration_file} reported no changes needed")
                    else:
                        logger.warning(f"Migration {migration_file} reported errors")
                        migration_errors = True
                else:
                    logger.warning(f"Migration {migration_file} doesn't have run_migration() function")
            else:
                logger.warning(f"Could not load migration module: {migration_file}")
        except Exception as e:
            logger.error(f"Error running migration {migration_file}: {str(e)}")
            migration_errors = True
    
    if migration_errors:
        logger.warning("Some migrations encountered errors, but the application will continue to start")
    else:
        logger.info("All migrations completed")

def create_app(config=None):
    """
    Factory pattern for creating the Flask app
    """
    # Load environment variables
    load_dotenv()

    # Create Flask app
    app = Flask(__name__, instance_relative_config=True)
    
    # Configure ProxyFix for reverse proxy (e.g., Nginx)
    app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
    
    # Create custom HTTPS middleware (will be applied below if USE_HTTPS is True)
    class ForceHTTPSMiddleware:
        """Middleware to force HTTPS scheme regardless of request headers"""
        def __init__(self, app):
            self.app = app
            
        def __call__(self, environ, start_response):
            # Force the wsgi.url_scheme to be https
            environ['wsgi.url_scheme'] = 'https'
            # Also override X-Forwarded-Proto if present
            environ['HTTP_X_FORWARDED_PROTO'] = 'https'
            return self.app(environ, start_response)
    
    # Create data directory if it doesn't exist
    data_dir = '/data'
    if not os.path.exists(data_dir):
        os.makedirs(data_dir, exist_ok=True)
      # Set the database file path in the data directory
    db_path = os.path.join(data_dir, 'song_data.db')

    # Configure the app
    app.config.from_object(Config)
    
    # Set preferred URL scheme for reverse proxy support
    if app.config.get('USE_HTTPS'):
        app.config['PREFERRED_URL_SCHEME'] = 'https'
        # Apply the HTTPS middleware when USE_HTTPS is True
        app.wsgi_app = ForceHTTPSMiddleware(app.wsgi_app)
        app.logger.info("Applying ForceHTTPSMiddleware - all URLs will use HTTPS scheme regardless of headers")
    
    # Explicitly set the database URI to ensure correct path
    app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
    
    # Initialize extensions with app
    db.init_app(app)
    csrf.init_app(app)
    
    # Register custom Jinja filters
    @app.template_filter('timestamp_to_datetime')
    def timestamp_to_datetime(timestamp):
        """Convert a Unix timestamp or ISO datetime string to a datetime object"""
        from datetime import datetime
        
        if isinstance(timestamp, str):
            try:
                # Try to parse as ISO format string
                return datetime.fromisoformat(timestamp)
            except (ValueError, TypeError):
                try:
                    # Try to convert to float first then use as timestamp
                    return datetime.fromtimestamp(float(timestamp))
                except (ValueError, TypeError):
                    return None
        elif timestamp is None:
            return None
        else:
            # Assume it's a numeric timestamp
            try:
                return datetime.fromtimestamp(timestamp)
            except (ValueError, TypeError):
                return None
    
    @app.template_filter('format_datetime')
    def format_datetime(dt, format='%Y-%m-%d %H:%M:%S'):
        """Format a datetime object to a string"""
        if not dt:
            return "Unknown"
        return dt.strftime(format)
    
    # Set up the Flask-Login extension
    login_manager.login_view = 'users.login'
    login_manager.login_message_category = 'info'
    login_manager.login_message = 'Please log in to access this page.'
    login_manager.init_app(app)
    
    # Add version info to template context
    @app.context_processor
    def inject_version():
        from musicround.version import get_version_str, VERSION_INFO
        return {
            'get_version_str': get_version_str,
            'version_info': VERSION_INFO
        }
    
    # Import User model here to avoid circular imports
    from musicround.models import User
    
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    
    # Ensure instance folder exists
    try:
        os.makedirs(app.instance_path, exist_ok=True)
    except OSError:
        pass
    
    # Define token handling functions within create_app
    def _app_fetch_token(name):
        app.logger.debug(f"_app_fetch_token: Called for service '{name}', user: {current_user.id if current_user.is_authenticated else 'Unauthenticated'}")
        if current_user.is_authenticated:
            if name == 'spotify':
                token_str = current_user.spotify_token
                app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Raw token string from DB: {token_str[:150] if token_str else 'None'}...")
                if token_str:
                    try:
                        token = json.loads(token_str)
                        app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Token after json.loads: {{'access_token': 'ACCESS_TOKEN_REDACTED', 'refresh_token': '{'REFRESH_TOKEN_REDACTED' if token.get('refresh_token') else 'None'}', 'expires_at': {token.get('expires_at')}, 'expires_in': {token.get('expires_in')}, 'scope': {token.get('scope')}, 'token_type': '{token.get('token_type')}'}}")

                        if 'refresh_token' not in token or not token.get('refresh_token'):
                            if hasattr(current_user, 'spotify_refresh_token') and current_user.spotify_refresh_token:
                                token['refresh_token'] = current_user.spotify_refresh_token
                                app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Added refresh_token from current_user.spotify_refresh_token.")
                            else:
                                app.logger.warning(f"_app_fetch_token for Spotify (user {current_user.id}): refresh_token missing in JSON and not found in current_user.spotify_refresh_token.")
                        
                        current_time = int(datetime.utcnow().timestamp())
                        if 'expires_at' in token:
                            if not isinstance(token['expires_at'], int):
                                try:
                                    token['expires_at'] = int(float(token['expires_at']))
                                    app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Converted existing expires_at to int: {token['expires_at']}")
                                except (ValueError, TypeError):
                                    app.logger.warning(f"_app_fetch_token for Spotify (user {current_user.id}): Could not convert existing expires_at '{token['expires_at']}' to int. Recalculating if possible.")
                                    if 'expires_in' in token and isinstance(token['expires_in'], (int, float)):
                                        token['expires_at'] = current_time + int(token['expires_in']) - 30
                                        app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Recalculated expires_at from expires_in: {token['expires_at']}")
                                    else:
                                        app.logger.error(f"_app_fetch_token for Spotify (user {current_user.id}): Cannot determine expires_at. Original problematic value: {token['expires_at']}")
                        elif 'expires_in' in token and isinstance(token['expires_in'], (int, float)):
                            token['expires_at'] = current_time + int(token['expires_in']) - 30
                            app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Calculated expires_at from expires_in: {token['expires_at']}")
                        elif hasattr(current_user, 'spotify_token_expires_at') and current_user.spotify_token_expires_at:
                            token['expires_at'] = int(current_user.spotify_token_expires_at.timestamp())
                            app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Used expires_at from current_user.spotify_token_expires_at: {token['expires_at']}")
                        else:
                            app.logger.warning(f"_app_fetch_token for Spotify (user {current_user.id}): expires_at missing and cannot be calculated.")

                        if 'token_type' not in token or not token.get('token_type'):
                            token['token_type'] = 'Bearer'
                            app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Set token_type to Bearer.")
                        
                        if 'expires_in' in token: 
                            del token['expires_in']

                        app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): Final token prepared for Authlib: {{'access_token': 'ACCESS_TOKEN_REDACTED', 'refresh_token': '{'REFRESH_TOKEN_REDACTED' if token.get('refresh_token') else 'None'}', 'expires_at': {token.get('expires_at')}, 'token_type': '{token.get('token_type')}', 'scope': {token.get('scope')}}}")
                        return token
                    except json.JSONDecodeError:
                        app.logger.error(f"_app_fetch_token for Spotify (user {current_user.id}): Failed to decode token JSON: {token_str[:100]}...")
                        return None
                    except Exception as e:
                        app.logger.error(f"_app_fetch_token for Spotify (user {current_user.id}): Error processing token: {str(e)}", exc_info=True)
                        return None
                else:
                    app.logger.debug(f"_app_fetch_token for Spotify (user {current_user.id}): No token string found in DB.")
                    return None
        app.logger.debug(f"_app_fetch_token: User not authenticated or service not matched for '{name}'.")
        return None

    def _app_update_token(name, token, refresh_token=None, access_token=None):
        app.logger.debug(f"_app_update_token: Called for service: {name}, user: {current_user.id if current_user.is_authenticated else 'Unauthenticated'}")
        if name == 'spotify':
            if current_user.is_authenticated:
                app.logger.info(f"_app_update_token for Spotify (user {current_user.id}): Received new token data to update. Keys: {list(token.keys()) if token else 'None'}")
                app.logger.debug(f"_app_update_token for Spotify (user {current_user.id}): Full new token: {{'access_token': 'ACCESS_TOKEN_REDACTED', 'refresh_token': '{'REFRESH_TOKEN_REDACTED' if token.get('refresh_token') else 'None'}', 'expires_at': {token.get('expires_at')}, 'token_type': '{token.get('token_type')}', 'scope': {token.get('scope')}}}")
                
                current_user.spotify_token = json.dumps(token)
                
                if 'expires_at' in token and token['expires_at'] is not None and hasattr(current_user, 'spotify_token_expires_at'):
                    try:
                        current_user.spotify_token_expires_at = datetime.fromtimestamp(int(token['expires_at']))
                        app.logger.debug(f"_app_update_token for Spotify (user {current_user.id}): Updated spotify_token_expires_at to {current_user.spotify_token_expires_at}")
                    except (TypeError, ValueError) as e:
                        app.logger.warning(f"_app_update_token for Spotify (user {current_user.id}): Could not update spotify_token_expires_at from token's expires_at ('{token['expires_at']}'): {str(e)}")

                if 'refresh_token' in token and token['refresh_token'] and hasattr(current_user, 'spotify_refresh_token'):
                    current_user.spotify_refresh_token = token['refresh_token']
                    app.logger.debug(f"_app_update_token for Spotify (user {current_user.id}): Updated spotify_refresh_token.")
                
                try:
                    db.session.commit()
                    app.logger.info(f"_app_update_token for Spotify (user {current_user.id}): Token successfully updated and committed to DB.")
                except Exception as e:
                    db.session.rollback()
                    app.logger.error(f"_app_update_token for Spotify (user {current_user.id}): Error committing token to DB: {str(e)}", exc_info=True)
            else:
                app.logger.warning(f"_app_update_token for Spotify: Attempted to update token for unauthenticated user.")
        # Add similar blocks for other services if needed

    # Initialize OAuth providers (Google, Authentik, Spotify via Authlib)
    from musicround.helpers.auth_helpers import init_oauth
    init_oauth(app) # This will use the imported oauth object

    # Manually register token handling functions
    app.logger.info(f"Attempting to manually register token functions. oauth object id: {id(oauth)}")
    if hasattr(oauth, 'tokengetter') and callable(oauth.tokengetter):
        oauth.tokengetter(_app_fetch_token)
        app.logger.info("SUCCESS: Manually registered _app_fetch_token using oauth.tokengetter().")
    else:
        app.logger.error("FAILURE: oauth.tokengetter method not found or not callable.")
        # Fallback for extreme cases - not recommended for production
        if isinstance(oauth, object) and hasattr(oauth, '_fetch_token_funcs') and isinstance(oauth._fetch_token_funcs, dict): # Basic check
             oauth._fetch_token_funcs['_app_fetch_token'] = _app_fetch_token
             app.logger.warning("MANUAL HACK: Injected _app_fetch_token into oauth._fetch_token_funcs.")
        else:
             app.logger.error("CRITICAL FAILURE: Cannot register fetch token function via method or hack.")


    if hasattr(oauth, 'tokenupdater') and callable(oauth.tokenupdater):
        oauth.tokenupdater(_app_update_token)
        app.logger.info("SUCCESS: Manually registered _app_update_token using oauth.tokenupdater().")
    else:
        app.logger.error("FAILURE: oauth.tokenupdater method not found or not callable.")
        if isinstance(oauth, object) and hasattr(oauth, '_update_token_funcs') and isinstance(oauth._update_token_funcs, dict): # Basic check
             oauth._update_token_funcs['_app_update_token'] = _app_update_token
             app.logger.warning("MANUAL HACK: Injected _app_update_token into oauth._update_token_funcs.")
        else:
            app.logger.error("CRITICAL FAILURE: Cannot register update token function via method or hack.")
            
    # Initialize Deezer client - import inside the function to avoid circular dependency
    from musicround.deezer_client import DeezerClient
    app.config['deezer'] = DeezerClient()
    
    # Register blueprints
    from musicround.routes.core import core_bp
    from musicround.routes.users import users_bp
    from musicround.routes.import_songs import import_songs_bp
    from musicround.routes.rounds import rounds_bp
    from musicround.routes.generate import generate_bp
    from musicround.routes.api import api_bp
    from musicround.routes.import_routes import import_bp
    from musicround.routes.process import process_bp
    from musicround.routes.deezer_routes import deezer_bp
    from musicround.routes.db_admin import db_admin_bp, init_admin
    from musicround.routes.auth import auth_bp
    from musicround.routes.oauth_debug import oauth_debug_bp
    
    app.register_blueprint(core_bp)
    app.register_blueprint(users_bp)
    app.register_blueprint(import_songs_bp)
    app.register_blueprint(rounds_bp)
    app.register_blueprint(generate_bp)
    app.register_blueprint(api_bp)
    app.register_blueprint(import_bp)
    app.register_blueprint(process_bp)
    app.register_blueprint(deezer_bp)
    app.register_blueprint(db_admin_bp)
    app.register_blueprint(auth_bp)
    app.register_blueprint(oauth_debug_bp)
    
    # Initialize the admin interface
    init_admin(app)
    
    # Register error handlers
    from musicround.errors import register_error_handlers
    register_error_handlers(app)

    # Initialize import queue and background workers
    from musicround.helpers.import_queue import ImportQueue, ImportWorker
    worker_count = int(os.environ.get('IMPORT_WORKER_COUNT', '2'))
    import_queue = ImportQueue()
    workers = [ImportWorker(app, import_queue) for _ in range(worker_count)]
    for w in workers:
        w.start()
    app.config['import_queue'] = import_queue
    app.config['import_workers'] = workers
    
    # Try to create database tables if they don't exist
    with app.app_context():
        try:
            db.create_all()
            logger.info("Database tables created successfully during app initialization")
            
            # Run migrations after tables are created
            run_migrations()
        except Exception as e:
            logger.error(f"Error creating database tables during app initialization: {e}")
    
    # Return the app
    return app

