"""Reports API endpoints."""

import csv
import io
from datetime import datetime, timezone
from flask import Blueprint, request, Response, current_app
from flask_jwt_extended import jwt_required

from shopdb.extensions import db
from shopdb.core.models import (
    Asset, AssetType, AssetStatus,
    Application
)
from shopdb.utils.responses import success_response, error_response, ErrorCodes
from shopdb.utils.authz import require_permission

reports_bp = Blueprint('reports', __name__)


def generate_csv(data: list, columns: list) -> str:
    """Generate CSV string from data."""
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(columns)
    for row in data:
        writer.writerow([row.get(col, '') for col in columns])
    return output.getvalue()


# =============================================================================
# Report: Machines by Type
# =============================================================================

@reports_bp.route('/machines-by-type', methods=['GET'])
@jwt_required(optional=True)
def machines_by_type():
    """
    Report: Machine count grouped by machine type.

    Query parameters:
    - businessunitid: Filter by business unit
    - format: 'json' (default) or 'csv'
    """
    try:
        from plugins.machines.models import Machine, MachineType
    except ImportError:
        return error_response(
            ErrorCodes.NOT_FOUND,
            'Machines plugin not installed',
            http_code=404
        )

    query = db.session.query(
        MachineType.machinetype,
        MachineType.description,
        db.func.count(Machine.machineid).label('count')
    ).outerjoin(Machine, Machine.machinetypeid == MachineType.machinetypeid
    ).outerjoin(Asset, Asset.assetid == Machine.assetid
    ).filter(
        db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
        MachineType.isactive == True
    )

    # Business unit filter
    if bu_id := request.args.get('businessunitid'):
        query = query.filter(Asset.businessunitid == int(bu_id))

    # Dualpath single-machine collapse: exclude the secondary bay so a dual-bay
    # pair counts once per type bucket. Gated on the site setting (default on).
    from shopdb.core.services.dualpath import (
        resolve_dualpath_pairs, dualpath_single_machine_enabled)
    if dualpath_single_machine_enabled():
        secondaryassetids = resolve_dualpath_pairs().secondaryassetids
        if secondaryassetids:
            query = query.filter(
                db.or_(Machine.assetid.is_(None),
                       Machine.assetid.notin_(secondaryassetids)))

    query = query.group_by(
        MachineType.machinetypeid,
        MachineType.machinetype,
        MachineType.description
    ).order_by(MachineType.machinetype)

    results = query.all()
    data = [{'machinetype': r.machinetype, 'description': r.description or '', 'count': r.count} for r in results]
    total = sum(r['count'] for r in data)

    if request.args.get('format') == 'csv':
        csv_data = generate_csv(data, ['machinetype', 'description', 'count'])
        return Response(
            csv_data,
            mimetype='text/csv',
            headers={'Content-Disposition': 'attachment; filename=machines_by_type.csv'}
        )

    return success_response({
        'report': 'machines_by_type',
        'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        'data': data,
        'total': total
    })


# =============================================================================
# Report: Assets by Status
# =============================================================================

@reports_bp.route('/assets-by-status', methods=['GET'])
@jwt_required(optional=True)
def assets_by_status():
    """
    Report: Asset count grouped by status.

    Query parameters:
    - assettypeid: Filter by asset type
    - businessunitid: Filter by business unit
    - format: 'json' (default) or 'csv'
    """
    query = db.session.query(
        AssetStatus.status,
        AssetStatus.color,
        db.func.count(Asset.assetid).label('count')
    ).outerjoin(Asset, Asset.statusid == AssetStatus.statusid
    ).filter(
        db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
        AssetStatus.isactive == True
    )

    # Asset type filter
    if type_id := request.args.get('assettypeid'):
        query = query.filter(Asset.assettypeid == int(type_id))

    # Business unit filter
    if bu_id := request.args.get('businessunitid'):
        query = query.filter(Asset.businessunitid == int(bu_id))

    query = query.group_by(
        AssetStatus.statusid,
        AssetStatus.status,
        AssetStatus.color
    ).order_by(AssetStatus.status)

    results = query.all()
    data = [{'status': r.status, 'color': r.color or '', 'count': r.count} for r in results]
    total = sum(r['count'] for r in data)

    if request.args.get('format') == 'csv':
        csv_data = generate_csv(data, ['status', 'count'])
        return Response(
            csv_data,
            mimetype='text/csv',
            headers={'Content-Disposition': 'attachment; filename=assets_by_status.csv'}
        )

    return success_response({
        'report': 'assets_by_status',
        'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        'data': data,
        'total': total
    })


# =============================================================================
# Report: KB Popularity
# =============================================================================

@reports_bp.route('/kb-popularity', methods=['GET'])
@jwt_required(optional=True)
def kb_popularity():
    """
    Report: Most clicked knowledge base articles.

    Query parameters:
    - limit: Number of results (default 20)
    - format: 'json' (default) or 'csv'
    """
    limit = min(int(request.args.get('limit', 20)), 100)

    # Knowledge Base is a plugin; degrade gracefully if it is not installed.
    try:
        from plugins.knowledgebase.models import KnowledgeBase
    except ImportError:
        return error_response(
            ErrorCodes.INTERNAL_ERROR,
            'KB popularity requires the knowledgebase plugin',
            http_code=503)

    articles = KnowledgeBase.query.filter(
        KnowledgeBase.isactive == True
    ).order_by(
        KnowledgeBase.clicks.desc()
    ).limit(limit).all()

    data = []
    for kb in articles:
        data.append({
            'linkid': kb.linkid,
            'shortdescription': kb.shortdescription,
            'application': kb.application.appname if kb.application else None,
            'clicks': kb.clicks or 0,
            'linkurl': kb.linkurl
        })

    if request.args.get('format') == 'csv':
        csv_data = generate_csv(data, ['linkid', 'shortdescription', 'application', 'clicks', 'linkurl'])
        return Response(
            csv_data,
            mimetype='text/csv',
            headers={'Content-Disposition': 'attachment; filename=kb_popularity.csv'}
        )

    return success_response({
        'report': 'kb_popularity',
        'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        'data': data,
        'total': len(data)
    })


# =============================================================================
# Report: Software Compliance
# =============================================================================

@reports_bp.route('/software-compliance', methods=['GET'])
@jwt_required(optional=True)
def software_compliance():
    """
    Report: Required applications vs installed (per PC).

    Shows which PCs have required applications installed.

    Query parameters:
    - appid: Filter to specific application
    - format: 'json' (default) or 'csv'
    """
    # Install tracking lives in the computers plugin; degrade gracefully if
    # it is not installed.
    try:
        from plugins.computers.models import Computer, ComputerInstalledApp
    except ImportError:
        return error_response(
            ErrorCodes.INTERNAL_ERROR,
            'Software compliance requires the computers plugin',
            http_code=503)

    # Get required applications
    required_apps = Application.query.filter(
        Application.isactive == True,
        Application.isrequired == True
    ).all()

    if not required_apps:
        return success_response({
            'report': 'software_compliance',
            'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
            'data': [],
            'message': 'No required applications defined'
        })

    app_filter = request.args.get('appid')

    data = []
    for app in required_apps:
        if app_filter and str(app.appid) != app_filter:
            continue

        # Get all PCs (computers)
        total_pcs = Computer.query.join(
            Asset, Asset.assetid == Computer.assetid
        ).filter(Asset.isactive == True).count()

        # Get PCs with this app installed
        installed_count = db.session.query(ComputerInstalledApp).join(
            Computer, Computer.computerid == ComputerInstalledApp.computerid
        ).join(
            Asset, Asset.assetid == Computer.assetid
        ).filter(
            ComputerInstalledApp.appid == app.appid,
            ComputerInstalledApp.isactive == True,
            Asset.isactive == True
        ).count()

        # Get list of non-compliant PCs
        compliant_pc_ids = db.session.query(ComputerInstalledApp.computerid).filter(
            ComputerInstalledApp.appid == app.appid,
            ComputerInstalledApp.isactive == True
        ).subquery()

        non_compliant_pcs = Computer.query.join(
            Asset, Asset.assetid == Computer.assetid
        ).filter(
            Asset.isactive == True,
            ~Computer.computerid.in_(compliant_pc_ids)
        ).limit(100).all()

        compliance_rate = (installed_count / total_pcs * 100) if total_pcs > 0 else 0

        data.append({
            'appid': app.appid,
            'appname': app.appname,
            'totalpcs': total_pcs,
            'installed': installed_count,
            'notinstalled': total_pcs - installed_count,
            'compliancerate': round(compliance_rate, 1),
            'noncompliantpcs': [
                {'computerid': pc.computerid,
                 'hostname': pc.hostname or (pc.asset.assetnumber if pc.asset else None)}
                for pc in non_compliant_pcs
            ]
        })

    if request.args.get('format') == 'csv':
        # Simplified CSV (no nested data)
        csv_rows = []
        for item in data:
            csv_rows.append({
                'appid': item['appid'],
                'appname': item['appname'],
                'totalpcs': item['totalpcs'],
                'installed': item['installed'],
                'notinstalled': item['notinstalled'],
                'compliancerate': item['compliancerate']
            })
        csv_data = generate_csv(csv_rows, ['appid', 'appname', 'totalpcs', 'installed', 'notinstalled', 'compliancerate'])
        return Response(
            csv_data,
            mimetype='text/csv',
            headers={'Content-Disposition': 'attachment; filename=software_compliance.csv'}
        )

    return success_response({
        'report': 'software_compliance',
        'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        'data': data,
        'total': len(data)
    })


# =============================================================================
# Report: Asset Inventory Summary
# =============================================================================

@reports_bp.route('/asset-inventory', methods=['GET'])
@jwt_required(optional=True)
def asset_inventory():
    """
    Report: Complete asset inventory summary.

    Query parameters:
    - businessunitid: Filter by business unit
    - locationid: Filter by location
    - format: 'json' (default) or 'csv'
    """
    # By asset type
    by_type = db.session.query(
        AssetType.assettype,
        db.func.count(Asset.assetid).label('count')
    ).outerjoin(Asset, Asset.assettypeid == AssetType.assettypeid
    ).filter(
        db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
        AssetType.isactive == True
    )

    if bu_id := request.args.get('businessunitid'):
        by_type = by_type.filter(Asset.businessunitid == int(bu_id))
    if loc_id := request.args.get('locationid'):
        by_type = by_type.filter(Asset.locationid == int(loc_id))

    by_type = by_type.group_by(AssetType.assettype).all()

    # By status
    by_status = db.session.query(
        AssetStatus.status,
        AssetStatus.color,
        db.func.count(Asset.assetid).label('count')
    ).outerjoin(Asset, Asset.statusid == AssetStatus.statusid
    ).filter(
        db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
        AssetStatus.isactive == True
    )

    if bu_id := request.args.get('businessunitid'):
        by_status = by_status.filter(Asset.businessunitid == int(bu_id))
    if loc_id := request.args.get('locationid'):
        by_status = by_status.filter(Asset.locationid == int(loc_id))

    by_status = by_status.group_by(AssetStatus.status, AssetStatus.color).all()

    # By location
    from shopdb.core.models import Location
    by_location = db.session.query(
        Location.locationname,
        db.func.count(Asset.assetid).label('count')
    ).outerjoin(Asset, Asset.locationid == Location.locationid
    ).filter(
        db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
        Location.isactive == True
    )

    if bu_id := request.args.get('businessunitid'):
        by_location = by_location.filter(Asset.businessunitid == int(bu_id))

    by_location = by_location.group_by(Location.locationname).all()

    data = {
        'bytype': [{'type': r.assettype, 'count': r.count} for r in by_type],
        'bystatus': [{'status': r.status, 'color': r.color, 'count': r.count} for r in by_status],
        'bylocation': [{'location': r.locationname, 'count': r.count} for r in by_location]
    }

    total = sum(r['count'] for r in data['bytype'])

    if request.args.get('format') == 'csv':
        # Create multiple sections in CSV
        csv_output = io.StringIO()

        csv_output.write("Asset Inventory Report\n")
        csv_output.write(f"Generated: {datetime.now(timezone.utc).replace(tzinfo=None).isoformat()}\n\n")

        csv_output.write("By Type\n")
        csv_output.write("Type,Count\n")
        for r in data['bytype']:
            csv_output.write(f"{r['type']},{r['count']}\n")

        csv_output.write("\nBy Status\n")
        csv_output.write("Status,Count\n")
        for r in data['bystatus']:
            csv_output.write(f"{r['status']},{r['count']}\n")

        csv_output.write("\nBy Location\n")
        csv_output.write("Location,Count\n")
        for r in data['bylocation']:
            csv_output.write(f"{r['location']},{r['count']}\n")

        return Response(
            csv_output.getvalue(),
            mimetype='text/csv',
            headers={'Content-Disposition': 'attachment; filename=asset_inventory.csv'}
        )

    return success_response({
        'report': 'asset_inventory',
        'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        'data': data,
        'total': total
    })


# =============================================================================
# Available Reports List
# =============================================================================

# =============================================================================
# Report: PC-Machine Relationships
# =============================================================================

@reports_bp.route('/pc-relationships', methods=['GET'])
@jwt_required(optional=True)
def pc_relationships():
    """
    Report: PCs with relationships to shop floor machines.

    Returns machine number, vendor, model, PC hostname, and PC IP address
    for all active PC-machine relationships.

    Query parameters:
    - format: 'json' (default) or 'csv'
    """
    # PC <-> machine relationships in EITHER direction. The import orients edges
    # both ways: the general machinerelationships migration stores machine(source)
    # -> PC(target), while the synthetic measuring-tool links store PC(source) ->
    # tool(target). Match both so the report is not empty just because of edge
    # direction (this is a conceptually undirected "PC runs machine" report).
    # Each half joins a computer on one end and a machine on the other; the
    # machine end supplies machine_number/vendor/model, the PC end hostname/ip.
    half = """
        SELECT
            eq.assetnumber AS machine_number,
            v.vendor AS vendor,
            mo.modelnumber AS model,
            COALESCE(cpc.hostname, pc.assetnumber) AS hostname,
            c.ipaddress AS ip
        FROM assetrelationships ar
        JOIN computers cpc ON cpc.assetid = ar.{pc_end}
        JOIN assets pc ON pc.assetid = ar.{pc_end}
        JOIN machines eqx ON eqx.assetid = ar.{machine_end}
        JOIN assets eq ON eq.assetid = ar.{machine_end}
        LEFT JOIN communications c ON c.assetid = pc.assetid AND c.isprimary = 1
        LEFT JOIN models mo ON eqx.modelnumberid = mo.modelnumberid
        LEFT JOIN vendors v ON mo.vendorid = v.vendorid
        WHERE ar.isactive = 1
        AND eq.assetnumber IS NOT NULL AND eq.assetnumber != ''
    """
    sql = db.text(
        half.format(pc_end='sourceassetid', machine_end='targetassetid')
        + ' UNION '
        + half.format(pc_end='targetassetid', machine_end='sourceassetid')
        + ' ORDER BY machine_number'
    )

    results = db.session.execute(sql).fetchall()
    data = [{
        'machine_number': r.machine_number,
        'vendor': r.vendor or '',
        'model': r.model or '',
        'hostname': r.hostname or '',
        'ip': r.ip or ''
    } for r in results]

    if request.args.get('format') == 'csv':
        csv_data = generate_csv(data, ['machine_number', 'vendor', 'model', 'hostname', 'ip'])
        return Response(
            csv_data,
            mimetype='text/csv',
            headers={'Content-Disposition': 'attachment; filename=pc_relationships.csv'}
        )

    return success_response({
        'report': 'pc_relationships',
        'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        'data': data,
        'total': len(data)
    })


@reports_bp.route('/email', methods=['POST'])
@jwt_required()
@require_permission('reports.export')
def email_report():
    """Email a report's data as an HTML table (on-demand delivery).

    Request:
        {
            "subject": "Warranty Report",
            "columns": [{"key": "vendor", "label": "Vendor"}, ...],
            "rows": [{"vendor": "...", ...}, ...],
            "intro": "optional lead paragraph",
            "to": "addr@example.com"   // optional; defaults to alert_recipients
        }

    Recipients default to the site's Alert Recipients when `to` is omitted, so
    this doubles as the alert delivery path. There is no scheduler in this app:
    sending is on-demand. To automate it, point an external cron job at this
    endpoint using an API token (PAT) scoped to reports.export.
    """
    from shopdb.utils.mailer import get_smtp_config, render_table_email, try_send

    data = request.get_json() or {}
    subject = data.get('subject') or 'ShopDB report'
    columns = data.get('columns') or []
    rows = data.get('rows') or []

    config = get_smtp_config()
    recipient = data.get('to') or config['alert_recipients']

    if not config['enabled'] or not config['host']:
        return success_response(
            {'sent': False, 'reason': 'notconfigured'},
            message='Email is not configured (SMTP disabled or host unset).')
    if not recipient:
        return error_response(
            ErrorCodes.VALIDATION_ERROR,
            'No recipient. Provide "to" or set Alert Recipients.')

    html, text = render_table_email(
        subject, columns, rows, intro=data.get('intro'))
    ok, error = try_send(recipient, subject, html, text=text)
    if ok:
        return success_response({'sent': True}, message='Report emailed.')
    return success_response(
        {'sent': False, 'error': error},
        message='Report email failed: ' + (error or 'unknown error'))


@reports_bp.route('', methods=['GET'])
@jwt_required(optional=True)
def list_reports():
    """List all available reports."""
    reports = [
        {
            'id': 'machines-by-type',
            'name': 'Machines by Type',
            'description': 'Machine count grouped by machine type',
            'endpoint': '/api/reports/machines-by-type',
            'category': 'inventory'
        },
        {
            'id': 'assets-by-status',
            'name': 'Assets by Status',
            'description': 'Asset count grouped by status',
            'endpoint': '/api/reports/assets-by-status',
            'category': 'inventory'
        },
        {
            'id': 'kb-popularity',
            'name': 'KB Popularity',
            'description': 'Most clicked knowledge base articles',
            'endpoint': '/api/reports/kb-popularity',
            'category': 'usage'
        },
        {
            'id': 'software-compliance',
            'name': 'Software Compliance',
            'description': 'Required applications vs installed',
            'endpoint': '/api/reports/software-compliance',
            'category': 'compliance'
        },
        {
            'id': 'asset-inventory',
            'name': 'Asset Inventory Summary',
            'description': 'Complete asset inventory breakdown',
            'endpoint': '/api/reports/asset-inventory',
            'category': 'inventory'
        },
        {
            'id': 'pc-relationships',
            'name': 'PC-Machine Relationships',
            'description': 'PCs with relationships to shop floor machines',
            'endpoint': '/api/reports/pc-relationships',
            'category': 'inventory'
        }
    ]

    # Merge report cards contributed by enabled plugins (get_reports hook).
    # Same access pattern as dashboard.get_navigation: skip disabled plugins,
    # fail loud in dev/test, isolate a broken plugin in prod.
    pm = current_app.extensions.get('plugin_manager')
    if pm:
        for name, plugin in pm.get_all_plugins().items():
            if not pm.registry.is_enabled(name):
                continue
            try:
                for entry in plugin.get_reports() or []:
                    entry['plugin'] = name
                    reports.append(entry)
            except Exception:
                if current_app.config.get('DEBUG') or current_app.config.get('TESTING'):
                    raise
                current_app.logger.exception(
                    'Plugin %s get_reports failed', name)

    return success_response({
        'reports': reports,
        'total': len(reports)
    })
