"""Applications API endpoints."""

from flask import Blueprint, request
from flask_jwt_extended import jwt_required

from shopdb.extensions import db
from shopdb.core.models import (
    Application, AppVersion, AuditLog
)
from shopdb.utils.responses import (
    success_response,
    error_response,
    paginated_response,
    ErrorCodes
)
from shopdb.utils.pagination import get_pagination_params, paginate_query
from shopdb.utils.import_mode import apply_import_timestamps


def _computer_models():
    """Lazily import the computers plugin models, or None if unavailable.

    Application install-tracking is a join over the computers plugin's tables.
    Importing lazily keeps the applications API importable when the computers
    plugin is absent or disabled.
    """
    try:
        from plugins.computers.models import Computer, ComputerInstalledApp
        return Computer, ComputerInstalledApp
    except ImportError:
        return None


def _installed_count(appid):
    """Count active installs of an app, 0 when the computers plugin is absent."""
    models = _computer_models()
    if not models:
        return 0
    _, ComputerInstalledApp = models
    return ComputerInstalledApp.query.filter_by(appid=appid, isactive=True).count()


def _related_kb(appid):
    """Knowledge Base articles linked to this app, [] when the KB plugin is
    absent. Lazy + guarded so core stays decoupled from the plugin."""
    try:
        from plugins.knowledgebase.models import KnowledgeBase
    except ImportError:
        return []
    rows = KnowledgeBase.query.filter_by(appid=appid, isactive=True).order_by(
        KnowledgeBase.shortdescription).all()
    return [{'linkid': k.linkid, 'shortdescription': k.shortdescription,
             'linkurl': k.linkurl, 'keywords': k.keywords} for k in rows]


def _require_computer_models():
    """Resolve (Computer, ComputerInstalledApp) or a 503 response tuple.

    Usage: `models, err = _require_computer_models(); if err: return err`.
    """
    models = _computer_models()
    if not models:
        return None, error_response(
            ErrorCodes.INTERNAL_ERROR,
            'Install tracking requires the computers plugin',
            http_code=503)
    return models, None


from shopdb.utils.authz import require_permission

applications_bp = Blueprint('applications', __name__)


@applications_bp.route('', methods=['GET'])
@jwt_required(optional=True)
def list_applications():
    """List all applications."""
    page, per_page = get_pagination_params(request)

    query = Application.query

    if request.args.get('active', 'true').lower() != 'false':
        query = query.filter(Application.isactive == True)

    # Filter out hidden unless specifically requested
    if request.args.get('showhidden', 'false').lower() != 'true':
        query = query.filter(Application.ishidden == False)

    # Filter by installable
    if request.args.get('installable') is not None:
        installable = request.args.get('installable').lower() == 'true'
        query = query.filter(Application.isinstallable == installable)

    # Exact-match natural-key lookup for idempotent import (app name).
    if exactappname := request.args.get('appname'):
        query = query.filter(Application.appname == exactappname)

    if search := request.args.get('search'):
        query = query.filter(
            db.or_(
                Application.appname.ilike(f'%{search}%'),
                Application.appdescription.ilike(f'%{search}%')
            )
        )

    query = query.order_by(Application.appname)

    items, total = paginate_query(query, page, per_page)
    data = []
    for app in items:
        # to_dict already flattens supportteamname/teamurl/contacts.
        app_dict = app.to_dict()
        app_dict['installedcount'] = _installed_count(app.appid)
        data.append(app_dict)

    return paginated_response(data, page, per_page, total)


@applications_bp.route('/<int:app_id>', methods=['GET'])
@jwt_required(optional=True)
def get_application(app_id: int):
    """Get a single application with details."""
    app = db.session.get(Application, app_id)

    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    data = app.to_dict()
    data['versions'] = [v.to_dict() for v in app.versions.filter_by(isactive=True).order_by(AppVersion.version.desc()).all()]
    data['installedcount'] = _installed_count(app.appid)
    data['knowledgebase'] = _related_kb(app.appid)

    return success_response(data)


@applications_bp.route('', methods=['POST'])
@jwt_required()
@require_permission('applications.create')
def create_application():
    """Create a new application."""
    data = request.get_json()

    if not data or not data.get('appname'):
        return error_response(ErrorCodes.VALIDATION_ERROR, 'appname is required')

    if Application.query.filter_by(appname=data['appname']).first():
        return error_response(
            ErrorCodes.CONFLICT,
            f"Application '{data['appname']}' already exists",
            http_code=409
        )

    app = Application(
        appname=data['appname'],
        appdescription=data.get('appdescription'),
        supportteamid=data.get('supportteamid'),
        isinstallable=data.get('isinstallable', False),
        applicationnotes=data.get('applicationnotes'),
        installpath=data.get('installpath'),
        applicationlink=data.get('applicationlink'),
        documentationpath=data.get('documentationpath'),
        ishidden=data.get('ishidden', False),
        isprinter=data.get('isprinter', False),
        islicenced=data.get('islicenced', False),
        isrequired=data.get('isrequired', False),
        image=data.get('image')
    )

    db.session.add(app)
    apply_import_timestamps(app, data)
    db.session.flush()

    AuditLog.log('created', 'Application', entityid=app.appid, entityname=app.appname)

    db.session.commit()

    return success_response(app.to_dict(), message='Application created', http_code=201)


@applications_bp.route('/<int:app_id>', methods=['PUT'])
@jwt_required()
@require_permission('applications.edit')
def update_application(app_id: int):
    """Update an application."""
    app = db.session.get(Application, app_id)

    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    data = request.get_json()
    if not data:
        return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')

    if 'appname' in data and data['appname'] != app.appname:
        if Application.query.filter_by(appname=data['appname']).first():
            return error_response(
                ErrorCodes.CONFLICT,
                f"Application '{data['appname']}' already exists",
                http_code=409
            )

    fields = [
        'appname', 'appdescription', 'supportteamid', 'isinstallable',
        'applicationnotes', 'installpath', 'applicationlink', 'documentationpath',
        'ishidden', 'isprinter', 'islicenced', 'isrequired', 'image', 'isactive'
    ]

    changes = {}
    for key in fields:
        if key in data:
            old_val = getattr(app, key)
            new_val = data[key]
            if old_val != new_val:
                changes[key] = {'old': old_val, 'new': new_val}
            setattr(app, key, data[key])

    if changes:
        AuditLog.log('updated', 'Application', entityid=app.appid,
                     entityname=app.appname, changes=changes)

    apply_import_timestamps(app, data)
    db.session.commit()
    return success_response(app.to_dict(), message='Application updated')


@applications_bp.route('/<int:app_id>', methods=['DELETE'])
@jwt_required()
@require_permission('applications.delete')
def delete_application(app_id: int):
    """Delete (deactivate) an application."""
    app = db.session.get(Application, app_id)

    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    app.isactive = False

    AuditLog.log('deleted', 'Application', entityid=app.appid, entityname=app.appname)

    db.session.commit()

    return success_response(message='Application deleted')


# ---- Versions ----

@applications_bp.route('/<int:app_id>/versions', methods=['GET'])
@jwt_required(optional=True)
def list_versions(app_id: int):
    """List all versions for an application."""
    app = db.session.get(Application, app_id)
    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    versions = app.versions.filter_by(isactive=True).order_by(AppVersion.version.desc()).all()
    return success_response([v.to_dict() for v in versions])


@applications_bp.route('/<int:app_id>/versions', methods=['POST'])
@jwt_required()
@require_permission('applications.create')
def create_version(app_id: int):
    """Create a new version for an application."""
    app = db.session.get(Application, app_id)
    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    data = request.get_json()
    if not data or not data.get('version'):
        return error_response(ErrorCodes.VALIDATION_ERROR, 'version is required')

    if AppVersion.query.filter_by(appid=app_id, version=data['version']).first():
        return error_response(
            ErrorCodes.CONFLICT,
            f"Version '{data['version']}' already exists for this application",
            http_code=409
        )

    version = AppVersion(
        appid=app_id,
        version=data['version'],
        releasedate=data.get('releasedate'),
        notes=data.get('notes')
    )

    db.session.add(version)
    # Preserve the legacy release/added timestamps in import mode (no-op
    # otherwise), so imported version history keeps its original dates.
    apply_import_timestamps(version, data)
    db.session.commit()

    return success_response(version.to_dict(), message='Version created', http_code=201)


# ---- Computers with this app installed ----

@applications_bp.route('/<int:app_id>/installed', methods=['GET'])
@jwt_required(optional=True)
def list_installed_machines(app_id: int):
    """List all computers that have this application installed."""
    app = db.session.get(Application, app_id)
    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    models, err = _require_computer_models()
    if err:
        return err
    _, ComputerInstalledApp = models

    installed = ComputerInstalledApp.query.filter_by(
        appid=app_id, isactive=True).all()
    data = []
    for i in installed:
        comp = i.computer
        version = i.installedversion
        if not version and i.appversion:
            version = i.appversion.version
        item = {
            'id': i.id,
            'computerid': i.computerid,
            'version': version,
        }
        if comp:
            item['computer'] = {
                'computerid': comp.computerid,
                'assetnumber': comp.asset.assetnumber if comp.asset else None,
                'hostname': comp.hostname,
            }
        data.append(item)

    return success_response(data)


# ---- Installed Apps (per computer) ----

@applications_bp.route('/machines/<int:machine_id>', methods=['GET'])
@jwt_required(optional=True)
def list_machine_applications(machine_id: int):
    """List all applications installed on a computer."""
    models, err = _require_computer_models()
    if err:
        return err
    Computer, ComputerInstalledApp = models

    comp = db.session.get(Computer, machine_id)
    if not comp:
        return error_response(ErrorCodes.NOT_FOUND, 'Computer not found', http_code=404)

    installed = comp.installedapps.filter_by(isactive=True).all()
    return success_response([i.to_dict() for i in installed])


@applications_bp.route('/machines/<int:machine_id>', methods=['POST'])
@jwt_required()
@require_permission('applications.create')
def install_application(machine_id: int):
    """Install an application on a computer."""
    models, err = _require_computer_models()
    if err:
        return err
    Computer, ComputerInstalledApp = models

    comp = db.session.get(Computer, machine_id)
    if not comp:
        return error_response(ErrorCodes.NOT_FOUND, 'Computer not found', http_code=404)

    data = request.get_json()
    if not data or not data.get('appid'):
        return error_response(ErrorCodes.VALIDATION_ERROR, 'appid is required')

    app = db.session.get(Application, data['appid'])
    if not app:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404)

    existing = ComputerInstalledApp.query.filter_by(
        computerid=machine_id,
        appid=data['appid']
    ).first()

    if existing:
        if existing.isactive:
            return error_response(
                ErrorCodes.CONFLICT,
                'Application already installed on this computer',
                http_code=409
            )
        existing.isactive = True
        existing.appversionid = data.get('appversionid')
        existing.installeddate = db.func.now()
        db.session.commit()
        return success_response(existing.to_dict(), message='Application reinstalled')

    installed = ComputerInstalledApp(
        computerid=machine_id,
        appid=data['appid'],
        appversionid=data.get('appversionid')
    )

    db.session.add(installed)
    db.session.commit()

    return success_response(installed.to_dict(), message='Application installed', http_code=201)


@applications_bp.route('/machines/<int:machine_id>/<int:app_id>', methods=['DELETE'])
@jwt_required()
@require_permission('applications.delete')
def uninstall_application(machine_id: int, app_id: int):
    """Uninstall an application from a computer."""
    models, err = _require_computer_models()
    if err:
        return err
    _, ComputerInstalledApp = models

    installed = ComputerInstalledApp.query.filter_by(
        computerid=machine_id,
        appid=app_id,
        isactive=True
    ).first()

    if not installed:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not installed on this computer', http_code=404)

    installed.isactive = False
    db.session.commit()

    return success_response(message='Application uninstalled')


@applications_bp.route('/machines/<int:machine_id>/<int:app_id>', methods=['PUT'])
@jwt_required()
@require_permission('applications.edit')
def update_installed_app(machine_id: int, app_id: int):
    """Update installed application (e.g., change version)."""
    models, err = _require_computer_models()
    if err:
        return err
    _, ComputerInstalledApp = models

    installed = ComputerInstalledApp.query.filter_by(
        computerid=machine_id,
        appid=app_id,
        isactive=True
    ).first()

    if not installed:
        return error_response(ErrorCodes.NOT_FOUND, 'Application not installed on this computer', http_code=404)

    data = request.get_json()
    if not data:
        return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')

    if 'appversionid' in data:
        installed.appversionid = data['appversionid']

    db.session.commit()

    return success_response(installed.to_dict(), message='Installation updated')

# Support teams + contacts now live in the supportteams blueprint
# (/api/supportteams), replacing the legacy appowners pair.
