'use server';

import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import {
    CreateAssetRequest,
    UpdateAssetRequest,
    AssetQueryParams,
    CreateAssetAssociationRequest,
    CreateMaintenanceScheduleRequest,
    UpdateMaintenanceScheduleRequest,
    CreateMaintenanceHistoryRequest,
    CreateAssetRelationshipRequest,
    Asset,
    AssetListResponse,
    AssetAssociation,
    AssetHistory,
    AssetTicketSummary,
    AssetMaintenanceSchedule,
    AssetMaintenanceHistory,
    AssetMaintenanceReport,
    ClientMaintenanceSummary,
    AssetRelationship,
    WorkstationAsset,
    NetworkDeviceAsset,
    ServerAsset,
    MobileDeviceAsset,
    PrinterAsset,
    AssetSummaryMetrics,
    HealthStatus,
    SecurityStatus,
    WarrantyStatus,
    DeletionValidationResult,
    AssetFact,
} from '@alga-psa/types';
import type { IDocument } from '@alga-psa/types';
import { validateData } from '@alga-psa/validation';
import { deleteEntityWithValidation } from '@alga-psa/core';
import {
    assetSchema,
    assetAssociationSchema,
    createAssetSchema,
    createAssetAssociationSchema,
    updateAssetSchema,
    assetQuerySchema,
    assetMaintenanceScheduleSchema,
    createMaintenanceScheduleSchema,
    updateMaintenanceScheduleSchema,
    assetMaintenanceHistorySchema,
    createMaintenanceHistorySchema,
    assetMaintenanceReportSchema,
    clientMaintenanceSummarySchema,
    assetRelationshipSchema,
    createAssetRelationshipSchema
} from '../lib/schemas/asset.schema';
import { formatClientLocation, type ClientLocationLike } from '../lib/formatClientLocation';
import { withAuth, hasPermission } from '@alga-psa/auth';
import { createTenantKnex } from '@alga-psa/db';
import { Knex } from 'knex';
import { withTransaction } from '@alga-psa/db';
import { publishWorkflowEvent } from '@alga-psa/event-bus/publishers';
import {
    buildAssetAssignedPayload,
    buildAssetCreatedPayload,
    buildAssetUnassignedPayload,
    buildAssetUpdatedPayload,
    buildAssetWarrantyExpiringPayload,
    computeAssetWarrantyExpiring,
} from '@alga-psa/workflow-streams';
import {
    BuiltinAuthorizationKernelProvider,
    BundleAuthorizationKernelProvider,
    RequestLocalAuthorizationCache,
    createAuthorizationKernel,
    type AuthorizationRecord,
    type AuthorizationSubject,
} from '@alga-psa/authorization/kernel';
import { resolveBundleNarrowingRulesForEvaluation } from '@alga-psa/authorization/bundles/service';
import { buildAuthorizationAwarePage } from '@alga-psa/authorization/pagination';
import { listAvailableAssetFactsForAsset } from '../lib/assetFactsService';
import { getAssetTypeBySlug } from '../lib/assetTypeRegistry';
import {
    EXTENSION_TABLE_BY_ASSET_TYPE,
    attributeValidationError,
    invalidAssetTypeError,
    isBuiltinAssetTypeSlug,
    isTypedAssetWriteError,
    validateAttributesAgainstSchema,
} from '../lib/assetTypeAttributes';
import type { AssetTypeRegistryEntry } from '@alga-psa/types';

type AssetExtensionType = WorkstationAsset | NetworkDeviceAsset | ServerAsset | MobileDeviceAsset | PrinterAsset;

export interface AssetDetailBundle {
    asset: Asset;
    maintenanceReport: AssetMaintenanceReport;
    maintenanceHistory: AssetMaintenanceHistory[];
    history: AssetHistory[];
    tickets: AssetTicketSummary[];
    documents: IDocument[];
}

export interface BulkAssetActionResult {
    asset_id: string;
    success: boolean;
    asset?: Asset;
    error?: string;
}

export interface BulkAssetActionResponse {
    total: number;
    succeeded: number;
    failed: number;
    results: BulkAssetActionResult[];
}

const BULK_ASSET_ACTION_LIMIT = 100;
// Cap parallel per-asset work so a 100-item bulk doesn't seize the whole
// connection pool. Each iteration still opens its own transaction so partial
// failures stay isolated; this just bounds how many run at once.
const BULK_ASSET_ACTION_CONCURRENCY = 5;

async function mapWithConcurrency<T, R>(
    items: readonly T[],
    concurrency: number,
    fn: (item: T, index: number) => Promise<R>
): Promise<R[]> {
    const results = new Array<R>(items.length);
    let cursor = 0;
    const workerCount = Math.max(1, Math.min(concurrency, items.length));
    const workers = Array.from({ length: workerCount }, async () => {
        while (true) {
            const index = cursor++;
            if (index >= items.length) return;
            results[index] = await fn(items[index], index);
        }
    });
    await Promise.all(workers);
    return results;
}

const normalizeNullableString = (value: string | null | undefined) => (value ?? undefined);

function normalizeBulkAssetIds(assetIds: string[]): string[] {
    const ids = Array.from(new Set(assetIds.map((id) => id.trim()).filter(Boolean)));
    if (ids.length === 0) {
        throw new Error('Select at least one asset');
    }
    if (ids.length > BULK_ASSET_ACTION_LIMIT) {
        throw new Error(`Bulk actions are limited to ${BULK_ASSET_ACTION_LIMIT} assets at a time`);
    }
    z.array(z.string().uuid()).parse(ids);
    return ids;
}

function buildBulkAssetActionResponse(results: BulkAssetActionResult[]): BulkAssetActionResponse {
    const succeeded = results.filter((result) => result.success).length;
    return {
        total: results.length,
        succeeded,
        failed: results.length - succeeded,
        results,
    };
}

function pruneNullishValues(value: unknown): unknown {
    if (Array.isArray(value)) {
        return value
            .map((item) => pruneNullishValues(item))
            .filter((item) => item !== undefined);
    }

    if (value && typeof value === 'object') {
        const entries = Object.entries(value as Record<string, unknown>).reduce<Record<string, unknown>>((acc, [key, val]) => {
            const cleanedValue = pruneNullishValues(val);
            if (cleanedValue !== undefined) {
                acc[key] = cleanedValue;
            }
            return acc;
        }, {});

        return Object.keys(entries).length > 0 ? entries : undefined;
    }

    return value === null ? undefined : value;
}

function sanitizeUpdatePayload(data: UpdateAssetRequest): UpdateAssetRequest {
    const sanitized = {
        ...data,
        workstation: data.workstation ? (({ last_login, gpu_model, ...rest }) => ({
            ...rest,
            last_login: normalizeNullableString(last_login),
            gpu_model: normalizeNullableString(gpu_model)
        }))(data.workstation) : undefined,
        network_device: data.network_device,
        server: data.server ? (({ raid_config, hypervisor, primary_ip, ...rest }) => ({
            ...rest,
            raid_config: normalizeNullableString(raid_config),
            hypervisor: normalizeNullableString(hypervisor),
            primary_ip: normalizeNullableString(primary_ip)
        }))(data.server) : undefined,
        mobile_device: data.mobile_device ? (({ carrier, phone_number, ...rest }) => ({
            ...rest,
            carrier: normalizeNullableString(carrier),
            phone_number: normalizeNullableString(phone_number)
        }))(data.mobile_device) : undefined,
        printer: data.printer ? (({ ip_address, ...rest }) => ({
            ...rest,
            ip_address: normalizeNullableString(ip_address)
        }))(data.printer) : undefined
    };

    const cleaned = pruneNullishValues(sanitized) as UpdateAssetRequest;
    if (data.location_id === null) {
        cleaned.location_id = null;
    }
    return cleaned;
}

type AssetLocationRow = ClientLocationLike & { location_id: string };

async function resolveValidatedAssetLocation(
    trx: Knex.Transaction,
    tenant: string,
    clientId: string,
    locationId: string | null | undefined
): Promise<{ location_id: string | null; location?: string } | undefined> {
    if (locationId === undefined) {
        return undefined;
    }

    if (locationId === null) {
        return { location_id: null };
    }

    const location = await trx('client_locations')
        .where({
            tenant,
            client_id: clientId,
            location_id: locationId,
            is_active: true,
        })
        .first<AssetLocationRow>();

    if (!location) {
        throw new Error('Selected location is not available for this client');
    }

    return {
        location_id: location.location_id,
        location: formatClientLocation(location),
    };
}

type AssetAuthUser = {
    user_id: string;
    user_type: 'internal' | 'client';
    clientId?: string | null;
    roles?: Array<{ role_id?: string } | string>;
};

function extractRoleIdsFromUser(user: AssetAuthUser): string[] {
    if (!Array.isArray(user.roles)) {
        return [];
    }

    return user.roles
        .map((role) => {
            if (typeof role === 'string') {
                return role;
            }
            return typeof role?.role_id === 'string' ? role.role_id : null;
        })
        .filter((value): value is string => Boolean(value));
}

async function resolveAuthorizationSubjectForUser(
    trx: Knex.Transaction,
    tenant: string,
    user: AssetAuthUser
): Promise<AuthorizationSubject> {
    let roleIds = extractRoleIdsFromUser(user);
    if (roleIds.length === 0) {
        try {
            const roleRows = await trx('user_roles')
                .where({ tenant, user_id: user.user_id })
                .select<{ role_id: string }[]>('role_id');
            roleIds = roleRows.map((row) => row.role_id);
        } catch {
            roleIds = [];
        }
    }

    const [teamRows, managedRows] = await Promise.all([
        trx('team_members').where({ tenant, user_id: user.user_id }).select<{ team_id: string }[]>('team_id').catch(() => []),
        trx('users').where({ tenant, reports_to: user.user_id }).select<{ user_id: string }[]>('user_id').catch(() => []),
    ]);

    return {
        tenant,
        userId: user.user_id,
        userType: user.user_type,
        roleIds,
        teamIds: teamRows.map((row) => row.team_id),
        managedUserIds: managedRows.map((row) => row.user_id),
        clientId: user.clientId ?? null,
        portfolioClientIds: user.clientId ? [user.clientId] : [],
    };
}

async function resolveAssetAuthorizationRecords(
    trx: Knex.Transaction,
    tenant: string,
    assets: Array<Pick<Asset, 'asset_id' | 'client_id'>>
): Promise<Map<string, AuthorizationRecord>> {
    const assetIds = assets.map((asset) => asset.asset_id);
    if (assetIds.length === 0) {
        return new Map();
    }

    const associationRows = await trx('asset_associations')
        .where({ tenant })
        .whereIn('asset_id', assetIds)
        .select<Array<{ asset_id: string; entity_type: string; entity_id: string }>>('asset_id', 'entity_type', 'entity_id');

    const associationsByAsset = new Map<string, Array<{ entity_type: string; entity_id: string }>>();
    for (const row of associationRows) {
        const existing = associationsByAsset.get(row.asset_id) ?? [];
        existing.push({ entity_type: row.entity_type, entity_id: row.entity_id });
        associationsByAsset.set(row.asset_id, existing);
    }

    const records = new Map<string, AuthorizationRecord>();
    for (const asset of assets) {
        const associations = associationsByAsset.get(asset.asset_id) ?? [];
        const assignedUserIds = associations
            .filter((association) => association.entity_type === 'user')
            .map((association) => association.entity_id);
        const teamIds = associations
            .filter((association) => association.entity_type === 'team')
            .map((association) => association.entity_id);

        records.set(asset.asset_id, {
            id: asset.asset_id,
            ownerUserId: assignedUserIds[0] ?? null,
            assignedUserIds,
            clientId: asset.client_id ?? null,
            teamIds,
        });
    }

    return records;
}

type AssetAuthorizationInput = Pick<Asset, 'asset_id' | 'client_id'>;

type AssetReadAuthorizationContext = {
    subject: AuthorizationSubject;
    authorizationKernel: ReturnType<typeof createAuthorizationKernel>;
    requestCache: RequestLocalAuthorizationCache;
};

function createAssetReadAuthorizationKernel(trx: Knex.Transaction) {
    return createAuthorizationKernel({
        builtinProvider: new BuiltinAuthorizationKernelProvider(),
        bundleProvider: new BundleAuthorizationKernelProvider({
            resolveRules: async (input) => {
                try {
                    return await resolveBundleNarrowingRulesForEvaluation(trx, input);
                } catch {
                    return [];
                }
            },
        }),
        rbacEvaluator: async () => true,
    });
}

async function createAssetReadAuthorizationContext(
    trx: Knex.Transaction,
    tenant: string,
    user: AssetAuthUser
): Promise<AssetReadAuthorizationContext> {
    return {
        subject: await resolveAuthorizationSubjectForUser(trx, tenant, user),
        authorizationKernel: createAssetReadAuthorizationKernel(trx),
        requestCache: new RequestLocalAuthorizationCache(),
    };
}

async function authorizeAssetReadDecision(
    trx: Knex.Transaction,
    tenant: string,
    context: AssetReadAuthorizationContext,
    asset: AssetAuthorizationInput
) {
    const assetRecords = await resolveAssetAuthorizationRecords(trx, tenant, [asset]);
    return context.authorizationKernel.authorizeResource({
        subject: context.subject,
        resource: {
            type: 'asset',
            action: 'read',
            id: asset.asset_id,
        },
        record: assetRecords.get(asset.asset_id),
        requestCache: context.requestCache,
        knex: trx,
    });
}

async function assertAssetReadAllowed(
    trx: Knex.Transaction,
    tenant: string,
    context: AssetReadAuthorizationContext,
    asset: AssetAuthorizationInput
): Promise<void> {
    const decision = await authorizeAssetReadDecision(trx, tenant, context, asset);
    if (!decision.allowed) {
        throw new Error('Permission denied: Cannot read assets');
    }
}

async function resolveAssetAuthorizationInputById(
    trx: Knex.Transaction,
    tenant: string,
    asset_id: string
): Promise<AssetAuthorizationInput> {
    const asset = await trx('assets')
        .where({ tenant, asset_id })
        .select('asset_id', 'client_id')
        .first() as AssetAuthorizationInput | undefined;

    if (!asset) {
        throw new Error('Asset not found');
    }

    return asset;
}

async function assertAssetReadAllowedById(
    trx: Knex.Transaction,
    tenant: string,
    context: AssetReadAuthorizationContext,
    asset_id: string
): Promise<AssetAuthorizationInput> {
    const asset = await resolveAssetAuthorizationInputById(trx, tenant, asset_id);
    await assertAssetReadAllowed(trx, tenant, context, asset);
    return asset;
}

async function createAuthorizedAssetReadContextForUser(
    trx: Knex.Transaction,
    tenant: string,
    user: AssetAuthUser,
    asset_id: string
): Promise<{ context: AssetReadAuthorizationContext; asset: AssetAuthorizationInput }> {
    const context = await createAssetReadAuthorizationContext(trx, tenant, user);
    const asset = await assertAssetReadAllowedById(trx, tenant, context, asset_id);
    return { context, asset };
}

async function getAuthorizedAssetIdsForClient(
    trx: Knex.Transaction,
    tenant: string,
    context: AssetReadAuthorizationContext,
    client_id: string
): Promise<string[]> {
    const assets = await trx('assets')
        .where({ tenant, client_id })
        .select('asset_id', 'client_id') as AssetAuthorizationInput[];

    if (assets.length === 0) {
        return [];
    }

    const decisions = await Promise.all(
        assets.map((asset) => authorizeAssetReadDecision(trx, tenant, context, asset))
    );

    return assets
        .filter((_, index) => decisions[index]?.allowed)
        .map((asset) => asset.asset_id);
}

function collectAssetUpdatedPaths(validatedData: Record<string, unknown>): string[] {
    const extensionKeys = new Set(['workstation', 'network_device', 'server', 'mobile_device', 'printer']);
    const paths: string[] = [];

    for (const [key, value] of Object.entries(validatedData)) {
        if (value === undefined) continue;

        if (extensionKeys.has(key) && value && typeof value === 'object' && !Array.isArray(value)) {
            for (const nestedKey of Object.keys(value as Record<string, unknown>)) {
                paths.push(`${key}.${nestedKey}`);
            }
            continue;
        }

        paths.push(key);
    }

    return paths;
}

// Helper function to get extension table data
async function getExtensionData(knex: Knex, tenant: string, asset_id: string, asset_type: string | undefined): Promise<AssetExtensionType | null> {
    if (!asset_type) return null;
    
    switch (asset_type.toLowerCase()) {
        case 'workstation':
            return knex('workstation_assets')
                .where({ tenant, asset_id })
                .first() as Promise<WorkstationAsset>;
        case 'network_device':
            return knex('network_device_assets')
                .where({ tenant, asset_id })
                .first() as Promise<NetworkDeviceAsset>;
        case 'server':
            return knex('server_assets')
                .where({ tenant, asset_id })
                .first() as Promise<ServerAsset>;
        case 'mobile_device':
            return knex('mobile_device_assets')
                .where({ tenant, asset_id })
                .first() as Promise<MobileDeviceAsset>;
        case 'printer':
            return knex('printer_assets')
                .where({ tenant, asset_id })
                .first() as Promise<PrinterAsset>;
        default:
            return null;
    }
}

// F310: asset_type must be one of the six built-ins or a tenant registry slug.
// Returns the registry entry when (and only when) the slug is a custom type.
async function resolveWritableAssetType(
    knex: Knex,
    tenant: string,
    assetType: string
): Promise<AssetTypeRegistryEntry | null> {
    if (isBuiltinAssetTypeSlug(assetType)) {
        return null;
    }
    const entry = await getAssetTypeBySlug(knex, tenant, assetType);
    if (!entry) {
        throw invalidAssetTypeError(assetType);
    }
    return entry.is_builtin ? null : entry;
}

// Helper function to insert/update extension table data.
// Trusts upstream Zod validation (createAssetSchema / updateAssetSchema in the action layer).
async function upsertExtensionData(
    knex: Knex,
    tenant: string,
    asset_id: string,
    asset_type: string | undefined,
    data: unknown
): Promise<void> {
    if (!asset_type || !data || typeof data !== 'object') return;

    const table = EXTENSION_TABLE_BY_ASSET_TYPE[asset_type.toLowerCase()];
    if (!table) return;
    // Strip tenant/asset_id — they're the Citus partition key + PK and cannot appear in an UPDATE SET clause.
    const { tenant: _t, asset_id: _a, ...extensionFields } = data as Record<string, unknown>;

    // Check if record exists
    const exists = await knex(table)
        .where({ tenant, asset_id })
        .first();

    if (exists) {
        if (Object.keys(extensionFields).length === 0) return;
        await knex(table)
            .where({ tenant, asset_id })
            .update(extensionFields);
    } else {
        await knex(table).insert({ tenant, asset_id, ...extensionFields });
    }
}

async function deleteExtensionData(
    knex: Knex,
    tenant: string,
    asset_id: string,
    asset_type: string | undefined
): Promise<void> {
    if (!asset_type) {
        return;
    }

    // 'unknown' and custom registry slugs carry no extension table.
    const table = EXTENSION_TABLE_BY_ASSET_TYPE[asset_type.toLowerCase()];
    if (!table) return;
    await knex(table)
        .where({ tenant, asset_id })
        .delete();
}

// Export getAsset for external use
export const getAsset = withAuth(async (user, { tenant }, asset_id: string): Promise<Asset> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset reading
    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read assets');
    }

    const asset = await getAssetWithExtensions(knex, tenant, asset_id);
    await withTransaction(knex, async (trx: Knex.Transaction) => {
        const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
        await assertAssetReadAllowed(trx, tenant, context, { asset_id: asset.asset_id, client_id: asset.client_id });
    });

    return asset;
});

export const getAvailableAssetFacts = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetFact[]> => {
    const { knex } = await createTenantKnex();
    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read asset facts');
    }

    return withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetFact[]> => {
        await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);
        return listAvailableAssetFactsForAsset(trx, { tenant, assetId: asset_id });
    });
});

export const getAssetDetailBundle = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetDetailBundle> => {
    const { knex } = await createTenantKnex();

    if (!await hasPermission(user, 'asset', 'read', knex)) {
        throw new Error('Permission denied: Cannot read assets');
    }

    return withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetDetailBundle> => {
        const [assetRecord, canReadTickets, canReadDocuments] = await Promise.all([
            getAssetWithExtensions(trx, tenant, asset_id),
            hasPermission(user, 'ticket', 'read', trx),
            hasPermission(user, 'document', 'read', trx)
        ]);

        const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
        await assertAssetReadAllowed(
            trx,
            tenant,
            context,
            { asset_id: assetRecord.asset_id, client_id: assetRecord.client_id }
        );

        const formattedAsset = formatAssetForOutput(assetRecord);

        // Structural child data inherits parent-asset authorization. Linked ticket/document data
        // additionally intersects with their own resource-family authorization.
        const [maintenanceReport, history, tickets, documents] = await Promise.all([
            fetchAssetMaintenanceReport(trx, tenant, asset_id),
            fetchAssetHistory(trx, tenant, asset_id),
            canReadTickets ? fetchAssetLinkedTickets(trx, tenant, asset_id, context) : Promise.resolve([]),
            canReadDocuments ? fetchAssetDocuments(trx, tenant, asset_id, assetRecord.client_id ?? null, 15, context) : Promise.resolve([])
        ]);

        return {
            asset: formattedAsset,
            maintenanceReport,
            maintenanceHistory: maintenanceReport?.maintenance_history ?? [],
            history,
            tickets,
            documents
        };
    });
});

function formatAssetForOutput(asset: any): Asset {
    // Format base asset data
    const formattedAsset = {
        ...asset,
        // Format dates
        created_at: typeof asset.created_at === 'string'
            ? asset.created_at
            : new Date(asset.created_at).toISOString(),
        updated_at: typeof asset.updated_at === 'string'
            ? asset.updated_at
            : new Date(asset.updated_at).toISOString(),
        purchase_date: asset.purchase_date
            ? new Date(asset.purchase_date).toISOString()
            : undefined,
        warranty_end_date: asset.warranty_end_date
            ? new Date(asset.warranty_end_date).toISOString()
            : undefined,
        // Handle optional fields
        serial_number: asset.serial_number || undefined,
        location: asset.location || undefined,
        location_id: asset.location_id ?? null,
        // Ensure client data is properly structured
        // The SQL JOIN returns client_name as a flat field; build the nested object from it
        client: asset.client_id ? {
            client_id: asset.client_id,
            client_name: asset.client_name || ''
        } : undefined,
        // Ensure relationships is always an array
        relationships: Array.isArray(asset.relationships) ? asset.relationships : [],

        // Format extension data based on asset type
        ...(asset.workstation && {
            workstation: {
                ...asset.workstation,
                cpu_cores: Number(asset.workstation.cpu_cores) || 0,
                ram_gb: Number(asset.workstation.ram_gb) || 0,
                storage_capacity_gb: Number(asset.workstation.storage_capacity_gb) || 0,
                gpu_model: asset.workstation.gpu_model || undefined,
                last_login: asset.workstation.last_login
                    ? new Date(asset.workstation.last_login).toISOString()
                    : undefined,
                installed_software: Array.isArray(asset.workstation.installed_software)
                    ? asset.workstation.installed_software
                    : []
            }
        }),

        ...(asset.network_device && {
            network_device: {
                ...asset.network_device,
                port_count: Number(asset.network_device.port_count) || 0,
                power_draw_watts: Number(asset.network_device.power_draw_watts) || 0,
                vlan_config: asset.network_device.vlan_config || {},
                port_config: asset.network_device.port_config || {}
            }
        }),

        ...(asset.server && {
            server: {
                ...asset.server,
                cpu_cores: Number(asset.server.cpu_cores) || 0,
                ram_gb: Number(asset.server.ram_gb) || 0,
                storage_config: Array.isArray(asset.server.storage_config)
                    ? asset.server.storage_config
                    : [],
                network_interfaces: Array.isArray(asset.server.network_interfaces)
                    ? asset.server.network_interfaces
                    : [],
                installed_services: Array.isArray(asset.server.installed_services)
                    ? asset.server.installed_services
                    : [],
                raid_config: asset.server.raid_config || undefined,
                hypervisor: asset.server.hypervisor || undefined,
                primary_ip: asset.server.primary_ip || undefined
            }
        }),

        ...(asset.mobile_device && {
            mobile_device: {
                ...asset.mobile_device,
                imei: asset.mobile_device.imei || undefined,
                phone_number: asset.mobile_device.phone_number || undefined,
                carrier: asset.mobile_device.carrier || undefined,
                last_check_in: asset.mobile_device.last_check_in
                    ? new Date(asset.mobile_device.last_check_in).toISOString()
                    : undefined,
                installed_apps: Array.isArray(asset.mobile_device.installed_apps)
                    ? asset.mobile_device.installed_apps
                    : []
            }
        }),

        ...(asset.printer && {
            printer: {
                ...asset.printer,
                ip_address: asset.printer.ip_address || undefined,
                max_paper_size: asset.printer.max_paper_size != null ? Number(asset.printer.max_paper_size) : undefined,
                monthly_duty_cycle: asset.printer.monthly_duty_cycle != null ? Number(asset.printer.monthly_duty_cycle) : undefined,
                supported_paper_types: Array.isArray(asset.printer.supported_paper_types)
                    ? asset.printer.supported_paper_types
                    : [],
                supply_levels: asset.printer.supply_levels || {}
            }
        })
    };

    return formattedAsset;
}

export const createAsset = withAuth(async (
    user,
    { tenant },
    data: CreateAssetRequest,
    // Importers (e.g. Hudu) project best-effort attributes and must never fail
    // on a required custom field the source didn't supply; they pass false.
    options?: { requireCustomAttributes?: boolean }
): Promise<Asset> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset creation
    if (!await hasPermission(user, 'asset', 'create')) {
        throw new Error('Permission denied: Cannot create assets');
    }

    try {
        // Validate input data first
        try {
            validateData(createAssetSchema, data);
        } catch (error) {
            console.error('Input validation error:', error);
            throw new Error('Invalid input data: ' + (error instanceof Error ? error.message : 'Unknown error'));
        }

        // Start transaction
        const result = await knex.transaction(async (trx: Knex.Transaction) => {
            // Validate the input data
            const validatedData = validateData(createAssetSchema, data);

            // F310: built-in slug or registered custom slug only; custom types
            // get their attributes payload validated against fields_schema.
            const customTypeEntry = await resolveWritableAssetType(trx, tenant, validatedData.asset_type);
            if (customTypeEntry) {
                const issues = validateAttributesAgainstSchema(
                    customTypeEntry.fields_schema,
                    validatedData.attributes ?? {},
                    { requireAll: options?.requireCustomAttributes !== false }
                );
                if (issues.length > 0) {
                    throw attributeValidationError(issues);
                }
            }

            const now = new Date().toISOString();
            const selectedLocation = await resolveValidatedAssetLocation(
                trx,
                tenant,
                validatedData.client_id,
                validatedData.location_id
            );

            // Extract only the base asset fields and ensure dates are only included if they exist
            const baseAssetData = {
                tenant,
                asset_type: validatedData.asset_type,
                client_id: validatedData.client_id,
                asset_tag: validatedData.asset_tag,
                name: validatedData.name,
                status: validatedData.status,
                location_id: selectedLocation?.location_id ?? null,
                location: validatedData.location !== undefined && validatedData.location !== ''
                    ? validatedData.location
                    : (selectedLocation?.location ?? ''),
                serial_number: validatedData.serial_number || '',
                created_at: now,
                updated_at: now,
                // Only include dates if they exist
                ...(validatedData.purchase_date && {
                    purchase_date: validatedData.purchase_date
                }),
                ...(validatedData.warranty_end_date && {
                    warranty_end_date: validatedData.warranty_end_date
                }),
                ...(validatedData.attributes && {
                    attributes: JSON.stringify(validatedData.attributes)
                })
            };

            // Create base asset
            const [asset] = await trx('assets')
                .insert(baseAssetData)
                .returning('*');

            // Handle extension table data based on asset type
            const extensionData = data[data.asset_type as keyof CreateAssetRequest];
            if (extensionData) {
                await upsertExtensionData(trx, tenant, asset.asset_id, data.asset_type, extensionData);
            }

            // Create history record
            await trx('asset_history').insert({
                tenant,
                asset_id: asset.asset_id,
                changed_by: user.user_id,
                change_type: 'created',
                changes: validatedData,
                changed_at: now
            });

            // Get complete asset data including extension table data
            const completeAsset = await getAssetWithExtensions(trx, tenant, asset.asset_id);

            // Format the asset data properly before returning
            return formatAssetForOutput(completeAsset);
        });

        // Validate the formatted output
        try {
            const created = validateData(assetSchema, result) as Asset;
            const occurredAt = created.created_at || new Date().toISOString();

            await publishWorkflowEvent({
                eventType: 'ASSET_CREATED',
                payload: buildAssetCreatedPayload({
                    assetId: created.asset_id,
                    clientId: created.client_id || undefined,
                    createdByUserId: user.user_id,
                    createdAt: created.created_at || occurredAt,
                    assetType: created.asset_type,
                    serialNumber: created.serial_number,
                }),
                ctx: {
                    tenantId: tenant,
                    occurredAt,
                    actor: { actorType: 'USER', actorUserId: user.user_id },
                },
            });

            const warranty = computeAssetWarrantyExpiring({
                now: occurredAt,
                previousExpiresAt: undefined,
                newExpiresAt: created.warranty_end_date ?? null,
                windowDays: 30,
            });

            if (warranty) {
                await publishWorkflowEvent({
                    eventType: 'ASSET_WARRANTY_EXPIRING',
                    payload: buildAssetWarrantyExpiringPayload({
                        assetId: created.asset_id,
                        clientId: created.client_id || undefined,
                        ...warranty,
                    }),
                    ctx: {
                        tenantId: tenant,
                        occurredAt,
                        actor: { actorType: 'USER', actorUserId: user.user_id },
                    },
                });
            }

            return created;
        } catch (error) {
            console.error('Output validation error:', error);
            // Extract Zod validation details if available
            if (error && typeof error === 'object' && 'issues' in error) {
                const zodError = error as { issues: Array<{ path: (string | number)[]; message: string }> };
                const details = zodError.issues
                    .map(issue => `${issue.path.join('.')}: ${issue.message}`)
                    .join('; ');
                throw new Error(`Asset validation failed: ${details}`);
            }
            throw new Error('Server error: Invalid output data format');
        }

    } catch (error) {
        console.error('Error creating asset:', error);
        if (error instanceof Error) {
            throw error; // Preserve the original error message
        }
        throw new Error('Failed to create asset');
    }
});

export const updateAsset = withAuth(async (
    user,
    { tenant },
    asset_id: string,
    data: UpdateAssetRequest,
    opts?: { suppressRevalidate?: boolean }
): Promise<Asset> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot update assets');
    }

    try {
        const result = await knex.transaction(async (trx: Knex.Transaction) => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

            const normalizedData = sanitizeUpdatePayload(data);
            const validatedData = validateData(updateAssetSchema, normalizedData);
            const {
                workstation,
                network_device,
                server: serverExtension,
                mobile_device,
                printer,
                attributes: attributesPayload,
                ...baseCandidate
            } = validatedData;

            const beforeCompleteAsset = await getAssetWithExtensions(trx, tenant, asset_id);
            const beforeFormatted = formatAssetForOutput(beforeCompleteAsset) as unknown as Record<string, unknown>;

            const extensionPayloads = {
                workstation,
                network_device,
                server: serverExtension,
                mobile_device,
                printer
            };

            // Get current asset
            const asset = await trx('assets')
                .where({ tenant, asset_id })
                .first();

            if (!asset) {
                throw new Error('Asset not found');
            }

            // F310: validate a provided asset_type against built-ins/registry,
            // and validate a provided attributes payload against the (next)
            // custom type's fields_schema. Merge semantics: omitted keys keep
            // their stored values, so required presence is not re-enforced.
            let customTypeEntry: AssetTypeRegistryEntry | null = null;
            if (typeof baseCandidate.asset_type === 'string') {
                customTypeEntry = await resolveWritableAssetType(trx, tenant, baseCandidate.asset_type);
            } else if (
                attributesPayload !== undefined &&
                typeof asset.asset_type === 'string' &&
                !isBuiltinAssetTypeSlug(asset.asset_type)
            ) {
                customTypeEntry = await getAssetTypeBySlug(trx, tenant, asset.asset_type);
            }

            if (attributesPayload !== undefined && customTypeEntry) {
                const issues = validateAttributesAgainstSchema(
                    customTypeEntry.fields_schema,
                    attributesPayload,
                    { requireAll: false }
                );
                if (issues.length > 0) {
                    throw attributeValidationError(issues);
                }
            }

            const locationIdWasProvided = Object.prototype.hasOwnProperty.call(baseCandidate, 'location_id');
            const locationWasProvided = Object.prototype.hasOwnProperty.call(baseCandidate, 'location');
            if (locationIdWasProvided) {
                const selectedLocation = await resolveValidatedAssetLocation(
                    trx,
                    tenant,
                    (baseCandidate.client_id as string | undefined) ?? asset.client_id,
                    baseCandidate.location_id as string | null | undefined
                );

                if (selectedLocation) {
                    baseCandidate.location_id = selectedLocation.location_id;
                    // Caller-supplied free-text wins over the resolved address
                    // so a more specific in-room descriptor isn't overwritten.
                    if (!locationWasProvided) {
                        baseCandidate.location = selectedLocation.location ?? '';
                    }
                }
            } else if (baseCandidate.client_id && baseCandidate.client_id !== asset.client_id && asset.location_id) {
                baseCandidate.location_id = null;
                if (!Object.prototype.hasOwnProperty.call(baseCandidate, 'location')) {
                    baseCandidate.location = '';
                }
            }

            // Update base asset fields (excluding extension payloads)
            const baseUpdateData: Record<string, unknown> = {};
            Object.entries(baseCandidate).forEach(([key, value]) => {
                if (value !== undefined) {
                    baseUpdateData[key] = value;
                }
            });

            if (attributesPayload !== undefined) {
                // jsonb merge (not replace) so sibling namespaces like
                // hudu_fields survive partial attribute writes.
                baseUpdateData.attributes = trx.raw(
                    `coalesce(attributes, '{}'::jsonb) || ?::jsonb`,
                    JSON.stringify(attributesPayload)
                );
            }

            if (Object.keys(baseUpdateData).length > 0) {
                baseUpdateData.updated_at = trx.fn.now();
                await trx('assets')
                    .where({ tenant, asset_id })
                    .update(baseUpdateData);
            } else {
                await trx('assets')
                    .where({ tenant, asset_id })
                    .update({ updated_at: trx.fn.now() });
            }

            const nextAssetType = (baseCandidate.asset_type as string | undefined) ?? asset.asset_type;

            if (baseCandidate.asset_type && baseCandidate.asset_type !== asset.asset_type) {
                await deleteExtensionData(trx, tenant, asset_id, asset.asset_type);
            }

            const extensionData = nextAssetType
                ? extensionPayloads[nextAssetType as keyof typeof extensionPayloads]
                : undefined;

            if (extensionData) {
                await upsertExtensionData(trx, tenant, asset_id, nextAssetType, extensionData);
            }

            // Create history record
            await trx('asset_history').insert({
                tenant,
                asset_id,
                changed_by: user.user_id,
                change_type: 'updated',
                changes: validatedData,
                changed_at: knex.fn.now()
            });

            // Return normalized asset data for client consumption
            const completeAsset = await getAssetWithExtensions(trx, tenant, asset_id);
            const afterFormatted = formatAssetForOutput(completeAsset) as unknown as Record<string, unknown>;

            return {
                before: beforeFormatted,
                after: afterFormatted,
                validatedUpdate: validatedData as unknown as Record<string, unknown>,
            };
        });

        if (!opts?.suppressRevalidate) {
            revalidatePath('/assets');
            revalidatePath(`/assets/${asset_id}`);
            revalidatePath('/msp/assets');
            revalidatePath(`/msp/assets/${asset_id}`);
        }

        const updated = validateData(assetSchema, result.after) as Asset;
        const occurredAt = new Date().toISOString();

        const updatedPaths = collectAssetUpdatedPaths(result.validatedUpdate);
        const updatePayload = buildAssetUpdatedPayload({
            assetId: asset_id,
            before: result.before,
            after: result.after,
            updatedPaths,
            updatedByUserId: user.user_id,
            updatedAt: occurredAt,
        });

        if (updatePayload.updatedFields || updatePayload.changes) {
            await publishWorkflowEvent({
                eventType: 'ASSET_UPDATED',
                payload: updatePayload,
                ctx: {
                    tenantId: tenant,
                    occurredAt,
                    actor: { actorType: 'USER', actorUserId: user.user_id },
                },
            });
        }

        const previousClientId = (result.before as any)?.client_id as string | undefined;
        const newClientId = (result.after as any)?.client_id as string | undefined;
        if (previousClientId && newClientId && previousClientId !== newClientId) {
            await publishWorkflowEvent({
                eventType: 'ASSET_ASSIGNED',
                payload: buildAssetAssignedPayload({
                    assetId: asset_id,
                    previousOwnerType: 'client',
                    previousOwnerId: previousClientId,
                    newOwnerType: 'client',
                    newOwnerId: newClientId,
                    assignedAt: occurredAt,
                }),
                ctx: {
                    tenantId: tenant,
                    occurredAt,
                    actor: { actorType: 'USER', actorUserId: user.user_id },
                },
            });
        }

        const warranty = computeAssetWarrantyExpiring({
            now: occurredAt,
            previousExpiresAt: (result.before as any)?.warranty_end_date ?? null,
            newExpiresAt: (result.after as any)?.warranty_end_date ?? null,
            windowDays: 30,
        });

        if (warranty) {
            await publishWorkflowEvent({
                eventType: 'ASSET_WARRANTY_EXPIRING',
                payload: buildAssetWarrantyExpiringPayload({
                    assetId: asset_id,
                    clientId: newClientId || previousClientId,
                    ...warranty,
                }),
                ctx: {
                    tenantId: tenant,
                    occurredAt,
                    actor: { actorType: 'USER', actorUserId: user.user_id },
                },
            });
        }

        return updated;
    } catch (error) {
        console.error('Error updating asset:', error);
        if (error instanceof z.ZodError) {
            // Serialize field-level issues so the client can render them inline + in a toast
            // without losing the user's in-progress form data.
            throw new Error(JSON.stringify({
                kind: 'validation',
                issues: error.issues.map((issue) => ({
                    path: issue.path,
                    message: issue.message,
                    code: issue.code,
                })),
            }));
        }
        if (isTypedAssetWriteError(error)) {
            // invalid_asset_type / attribute-schema issues already carry a
            // structured message the client knows how to render.
            throw error;
        }
        throw new Error('Failed to update asset');
    }
});

export const deleteAsset = withAuth(async (
    user,
    { tenant },
    asset_id: string,
    opts?: { suppressRevalidate?: boolean }
): Promise<DeletionValidationResult & { success: boolean; deleted?: boolean }> => {
    try {
        const { knex } = await createTenantKnex();

        if (!await hasPermission(user, 'asset', 'delete')) {
            throw new Error('Permission denied: Cannot delete assets');
        }

        const result = await deleteEntityWithValidation('asset', asset_id, knex, tenant, async (trx, tenantId) => {
            await createAuthorizedAssetReadContextForUser(trx as Knex.Transaction, tenantId, user as AssetAuthUser, asset_id);

            const asset = await trx('assets')
                .where({ tenant: tenantId, asset_id })
                .first();

            if (!asset) {
                throw new Error('Asset not found');
            }

            // Citus does not enforce ON DELETE CASCADE across distributed
            // tables, so every FK pointing at assets must be cleared
            // explicitly here regardless of the migration's onDelete clause.

            await deleteExtensionData(trx, tenantId, asset_id, asset.asset_type);
            // Asset-type may be wrong/null on stale rows; sweep every known
            // extension subtype table so the asset row can actually delete.
            for (const subtypeTable of [
                'workstation_assets',
                'server_assets',
                'mobile_device_assets',
                'network_device_assets',
                'printer_assets',
            ]) {
                await trx(subtypeTable).where({ tenant: tenantId, asset_id }).delete();
            }

            await trx('asset_history').where({ tenant: tenantId, asset_id }).delete();
            await trx('asset_maintenance_history').where({ tenant: tenantId, asset_id }).delete();
            await trx('asset_maintenance_schedules').where({ tenant: tenantId, asset_id }).delete();
            await trx('asset_maintenance_notifications').where({ tenant: tenantId, asset_id }).delete();
            await trx('asset_ticket_associations').where({ tenant: tenantId, asset_id }).delete();
            await trx('asset_service_history').where({ tenant: tenantId, asset_id }).delete();
            await trx('asset_document_associations').where({ tenant: tenantId, asset_id }).delete();
            if (await trx.schema.hasTable('asset_facts')) {
                await trx('asset_facts').where({ tenant: tenantId, asset_id }).delete();
            }
            if (await trx.schema.hasTable('asset_software')) {
                await trx('asset_software').where({ tenant: tenantId, asset_id }).delete();
            }
            await trx('asset_relationships')
                .where({ tenant: tenantId, parent_asset_id: asset_id })
                .orWhere({ tenant: tenantId, child_asset_id: asset_id })
                .delete();
            await trx('asset_associations').where({ tenant: tenantId, asset_id }).delete();
            await trx('document_associations')
                .where({ tenant: tenantId, entity_type: 'asset', entity_id: asset_id })
                .delete();
            // Polymorphic link table — no FK, but otherwise leaves dangling rows.
            await trx('ticket_entity_links')
                .where({ tenant: tenantId, entity_type: 'asset', entity_id: asset_id })
                .delete();
            await trx('tenant_external_entity_mappings')
                .where({ tenant: tenantId, alga_entity_type: 'asset', alga_entity_id: asset_id })
                .delete();
            await trx('external_entity_mappings')
                .where({ tenant: tenantId, asset_id })
                .delete();
            await trx('import_job_items')
                .where({ tenant: tenantId, asset_id })
                .delete();

            await trx('assets')
                .where({ tenant: tenantId, asset_id })
                .delete();
        });

        if (result.deleted) {
            if (!opts?.suppressRevalidate) {
                revalidatePath('/assets');
                revalidatePath('/msp/assets');
                revalidatePath(`/assets/${asset_id}`);
                revalidatePath(`/msp/assets/${asset_id}`);
            }

            const occurredAt = new Date().toISOString();
            await publishWorkflowEvent({
                eventType: 'ASSET_DELETED',
                payload: {
                    assetId: asset_id,
                    userId: user.user_id,
                    timestamp: occurredAt,
                },
                ctx: {
                    tenantId: tenant,
                    occurredAt,
                    actor: { actorType: 'USER', actorUserId: user.user_id },
                },
                idempotencyKey: `asset_deleted:${asset_id}:${occurredAt}`,
            });
        }

        return {
            ...result,
            success: result.deleted === true,
            deleted: result.deleted
        };
    } catch (error) {
        console.error('Error deleting asset:', error);
        return {
            success: false,
            canDelete: false,
            code: 'VALIDATION_FAILED',
            message: error instanceof Error ? error.message : 'Failed to delete asset',
            dependencies: [],
            alternatives: []
        };
    }
});

// Each item in a bulk action commits independently so partial failures
// still report per-id success/failure. Path revalidation is deferred until
// the loop finishes so 100 updates produce 4 path invalidations, not 400.
export const bulkUpdateAssets = withAuth(async (
    user,
    _ctx,
    assetIds: string[],
    data: Pick<UpdateAssetRequest, 'status' | 'location' | 'location_id'>
): Promise<BulkAssetActionResponse> => {
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot update assets');
    }

    const ids = normalizeBulkAssetIds(assetIds);

    const results = await mapWithConcurrency<string, BulkAssetActionResult>(
        ids,
        BULK_ASSET_ACTION_CONCURRENCY,
        async (asset_id) => {
            try {
                const asset = await updateAsset(asset_id, data, { suppressRevalidate: true });
                return { asset_id, success: true, asset };
            } catch (error) {
                return {
                    asset_id,
                    success: false,
                    error: error instanceof Error ? error.message : 'Failed to update asset',
                };
            }
        }
    );

    if (results.some((result) => result.success)) {
        revalidatePath('/assets');
        revalidatePath('/msp/assets');
    }

    return buildBulkAssetActionResponse(results);
});

export const bulkDeleteAssets = withAuth(async (
    user,
    _ctx,
    assetIds: string[]
): Promise<BulkAssetActionResponse> => {
    if (!await hasPermission(user, 'asset', 'delete')) {
        throw new Error('Permission denied: Cannot delete assets');
    }

    const ids = normalizeBulkAssetIds(assetIds);

    const results = await mapWithConcurrency<string, BulkAssetActionResult>(
        ids,
        BULK_ASSET_ACTION_CONCURRENCY,
        async (asset_id) => {
            try {
                const result = await deleteAsset(asset_id, { suppressRevalidate: true });
                const success = result.success === true;
                return {
                    asset_id,
                    success,
                    error: success ? undefined : result.message,
                };
            } catch (error) {
                return {
                    asset_id,
                    success: false,
                    error: error instanceof Error ? error.message : 'Failed to delete asset',
                };
            }
        }
    );

    if (results.some((result) => result.success)) {
        revalidatePath('/assets');
        revalidatePath('/msp/assets');
    }

    return buildBulkAssetActionResponse(results);
});

async function getAssetWithExtensions(knex: Knex, tenant: string, asset_id: string): Promise<Asset> {
    // Get base asset data with client info
    const asset = await knex('assets')
        .select(
            'assets.*',
            'clients.client_name'
        )
        .leftJoin('clients', function(this: Knex.JoinClause) {
            this.on('clients.client_id', '=', 'assets.client_id')
                .andOn('clients.tenant', '=', 'assets.tenant');
        })
        .where({ 'assets.tenant': tenant, 'assets.asset_id': asset_id })
        .first();

    if (!asset) {
        throw new Error('Asset not found');
    }

    // Get extension table data if applicable
    const extensionData = await getExtensionData(knex, tenant, asset_id, asset.asset_type);

    // Get relationships (include related asset name)
    const rawRelationships = await knex('asset_relationships as ar')
        .select(
            'ar.*',
            'parent.name as parent_name',
            'child.name as child_name'
        )
        .leftJoin('assets as parent', function(this: Knex.JoinClause) {
            this.on('ar.parent_asset_id', '=', 'parent.asset_id')
                .andOn('ar.tenant', '=', 'parent.tenant');
        })
        .leftJoin('assets as child', function(this: Knex.JoinClause) {
            this.on('ar.child_asset_id', '=', 'child.asset_id')
                .andOn('ar.tenant', '=', 'child.tenant');
        })
        .where('ar.tenant', tenant)
        .andWhere(function(this: Knex.QueryBuilder) {
            this.where('ar.parent_asset_id', asset_id)
                .orWhere('ar.child_asset_id', asset_id);
        });

    const relationships = rawRelationships.map((rel: any): AssetRelationship => {
        const created_at = rel.created_at instanceof Date ? rel.created_at.toISOString() : rel.created_at;
        const updated_at = rel.updated_at instanceof Date ? rel.updated_at.toISOString() : rel.updated_at;
        const name = asset_id === rel.parent_asset_id ? (rel.child_name ?? '') : (rel.parent_name ?? '');

        return validateData(assetRelationshipSchema, {
            tenant: rel.tenant,
            parent_asset_id: rel.parent_asset_id,
            child_asset_id: rel.child_asset_id,
            relationship_type: rel.relationship_type,
            created_at,
            updated_at,
            name
        }) as AssetRelationship;
    });

    // Transform the data
    const transformedAsset: Asset = {
        ...asset,
        client: {
            client_id: asset.client_id,
            client_name: asset.client_name || ''
        },
        relationships: relationships || [],  // Ensure relationships is always an array
        // Add extension data under the appropriate key
        ...(extensionData ? {
            [asset.asset_type]: extensionData
        } : {})
    };

    return transformedAsset;
}

export const getAssetRelationships = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetRelationship[]> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset reading
    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read asset relationships');
    }

    return withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetRelationship[]> => {
        await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

        const rawRelationships = await trx('asset_relationships as ar')
            .select(
                'ar.*',
                'parent.name as parent_name',
                'child.name as child_name'
            )
            .leftJoin('assets as parent', function(this: Knex.JoinClause) {
                this.on('ar.parent_asset_id', '=', 'parent.asset_id')
                    .andOn('ar.tenant', '=', 'parent.tenant');
            })
            .leftJoin('assets as child', function(this: Knex.JoinClause) {
                this.on('ar.child_asset_id', '=', 'child.asset_id')
                    .andOn('ar.tenant', '=', 'child.tenant');
            })
            .where('ar.tenant', tenant)
            .andWhere(function(this: Knex.QueryBuilder) {
                this.where('ar.parent_asset_id', asset_id)
                    .orWhere('ar.child_asset_id', asset_id);
            })
            .orderBy('ar.created_at', 'desc');

        return rawRelationships.map((rel: any): AssetRelationship => {
            const created_at = rel.created_at instanceof Date ? rel.created_at.toISOString() : rel.created_at;
            const updated_at = rel.updated_at instanceof Date ? rel.updated_at.toISOString() : rel.updated_at;
            const name = asset_id === rel.parent_asset_id ? (rel.child_name ?? '') : (rel.parent_name ?? '');

            return validateData(assetRelationshipSchema, {
                tenant: rel.tenant,
                parent_asset_id: rel.parent_asset_id,
                child_asset_id: rel.child_asset_id,
                relationship_type: rel.relationship_type,
                created_at,
                updated_at,
                name
            }) as AssetRelationship;
        });
    });
});

export const createAssetRelationship = withAuth(async (user, { tenant }, data: CreateAssetRelationshipRequest): Promise<AssetRelationship> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating (relationships are an update operation)
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot create asset relationships');
    }

    const validated = validateData(createAssetRelationshipSchema, data);

    // Prevent self-link
    if (validated.parent_asset_id === validated.child_asset_id) {
        throw new Error('An asset cannot be related to itself');
    }

    const rel = await withTransaction(knex, async (trx: Knex.Transaction) => {
        const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
        await assertAssetReadAllowedById(trx, tenant, context, validated.parent_asset_id);
        await assertAssetReadAllowedById(trx, tenant, context, validated.child_asset_id);

        await trx('asset_relationships')
            .insert({
                tenant,
                parent_asset_id: validated.parent_asset_id,
                child_asset_id: validated.child_asset_id,
                relationship_type: validated.relationship_type,
                created_at: trx.fn.now(),
                updated_at: trx.fn.now()
            })
            .returning('*');

        // Hydrate name for response
        return trx('asset_relationships as ar')
            .select(
                'ar.*',
                'parent.name as parent_name',
                'child.name as child_name'
            )
            .leftJoin('assets as parent', function(this: Knex.JoinClause) {
                this.on('ar.parent_asset_id', '=', 'parent.asset_id')
                    .andOn('ar.tenant', '=', 'parent.tenant');
            })
            .leftJoin('assets as child', function(this: Knex.JoinClause) {
                this.on('ar.child_asset_id', '=', 'child.asset_id')
                    .andOn('ar.tenant', '=', 'child.tenant');
            })
            .where({
                'ar.tenant': tenant,
                'ar.parent_asset_id': validated.parent_asset_id,
                'ar.child_asset_id': validated.child_asset_id
            })
            .first();
    });

    if (!rel) {
        throw new Error('Failed to create asset relationship');
    }

    const created_at = rel.created_at instanceof Date ? rel.created_at.toISOString() : rel.created_at;
    const updated_at = rel.updated_at instanceof Date ? rel.updated_at.toISOString() : rel.updated_at;
    const name = validated.parent_asset_id === rel.parent_asset_id ? (rel.child_name ?? '') : (rel.parent_name ?? '');

    revalidatePath('/assets');
    revalidatePath(`/assets/${validated.parent_asset_id}`);
    revalidatePath(`/assets/${validated.child_asset_id}`);
    revalidatePath('/msp/assets');
    revalidatePath(`/msp/assets/${validated.parent_asset_id}`);
    revalidatePath(`/msp/assets/${validated.child_asset_id}`);

    return validateData(assetRelationshipSchema, {
        tenant: rel.tenant,
        parent_asset_id: rel.parent_asset_id,
        child_asset_id: rel.child_asset_id,
        relationship_type: rel.relationship_type,
        created_at,
        updated_at,
        name
    }) as AssetRelationship;
});

export const deleteAssetRelationship = withAuth(async (user, { tenant }, parent_asset_id: string, child_asset_id: string): Promise<void> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating (relationships are an update operation)
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot delete asset relationships');
    }

    await withTransaction(knex, async (trx: Knex.Transaction) => {
        const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
        await assertAssetReadAllowedById(trx, tenant, context, parent_asset_id);
        await assertAssetReadAllowedById(trx, tenant, context, child_asset_id);

        await trx('asset_relationships')
            .where({
                tenant,
                parent_asset_id,
                child_asset_id
            })
            .delete();
    });

    revalidatePath('/assets');
    revalidatePath(`/assets/${parent_asset_id}`);
    revalidatePath(`/assets/${child_asset_id}`);
    revalidatePath('/msp/assets');
    revalidatePath(`/msp/assets/${parent_asset_id}`);
    revalidatePath(`/msp/assets/${child_asset_id}`);
});

export const listAssets = withAuth(async (user, { tenant }, params: AssetQueryParams): Promise<AssetListResponse> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset reading
    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read assets');
    }

    try {
        // Validate query parameters
        const validatedParams = validateData(assetQuerySchema, params);

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            const resolveSortColumn = (sortBy?: string): string | null => {
                if (!sortBy) return null;
                const allowed: Record<string, string> = {
                    name: 'assets.name',
                    asset_tag: 'assets.asset_tag',
                    asset_type: 'assets.asset_type',
                    status: 'assets.status',
                    agent_status: 'assets.agent_status',
                    location: 'assets.location',
                    client_name: 'clients.client_name',
                    created_at: 'assets.created_at',
                };
                return allowed[sortBy] ?? null;
            };

            const applyFilters = (query: Knex.QueryBuilder) => {
                if (validatedParams.client_id) {
                    query.where('assets.client_id', validatedParams.client_id);
                }
                if (validatedParams.location_id) {
                    query.where('assets.location_id', validatedParams.location_id);
                }
                if (validatedParams.asset_type) {
                    query.where('assets.asset_type', validatedParams.asset_type);
                }
                if (validatedParams.status) {
                    query.where('assets.status', validatedParams.status);
                }
                if (validatedParams.search) {
                    const searchTerm = `%${validatedParams.search}%`;
                    query.where(function () {
                        this.whereILike('assets.name', searchTerm)
                            .orWhereILike('assets.asset_tag', searchTerm)
                            .orWhereILike('assets.serial_number', searchTerm)
                            .orWhereILike('assets.location', searchTerm)
                            .orWhereILike('clients.client_name', searchTerm);
                    });
                }
                if (validatedParams.agent_status) {
                    query.where('assets.agent_status', validatedParams.agent_status);
                }
                if (validatedParams.rmm_managed !== undefined) {
                    if (validatedParams.rmm_managed) {
                        query.whereNotNull('assets.rmm_provider').whereNotNull('assets.rmm_device_id');
                    } else {
                        query.where(function () {
                            this.whereNull('assets.rmm_provider').orWhereNull('assets.rmm_device_id');
                        });
                    }
                }
            };

            const applySort = (query: Knex.QueryBuilder) => {
                const sortColumn = resolveSortColumn(validatedParams.sort_by);
                const sortDirection = validatedParams.sort_direction ?? 'desc';
                if (sortColumn) {
                    if (sortColumn === 'clients.client_name' || sortColumn === 'assets.name') {
                        query.orderByRaw(`lower(${sortColumn}) ${sortDirection}`);
                    } else {
                        query.orderBy(sortColumn, sortDirection);
                    }
                    query.orderBy('assets.created_at', 'desc');
                    return;
                }

                query.orderBy('assets.created_at', 'desc');
            };

            const buildAssetListQuery = () => {
                const query = trx('assets')
                    .where('assets.tenant', tenant)
                    .leftJoin('clients', function(this: Knex.JoinClause) {
                        this.on('clients.client_id', '=', 'assets.client_id')
                            .andOn('clients.tenant', '=', 'assets.tenant')
                            .andOn('clients.tenant', '=', trx.raw('?', [tenant]));
                    });

                applyFilters(query);
                return query;
            };

            const fetchPage = async (sourcePage: number, sourceLimit: number) => {
                const offset = (sourcePage - 1) * sourceLimit;
                const countRow = await buildAssetListQuery()
                    .count<{ count: string }>('assets.asset_id as count')
                    .first();
                const rows = await buildAssetListQuery()
                    .select('assets.*', 'clients.client_name')
                    .modify(applySort)
                    .limit(sourceLimit)
                    .offset(offset);

                return {
                    data: rows,
                    total: Number(countRow?.count || 0),
                };
            };

            const page = validatedParams.page || 1;
            const limit = validatedParams.limit || 10;
            const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
            const authorizedPage = await buildAuthorizationAwarePage<any>({
                page,
                limit,
                fetchPage,
                authorizeRecord: async (asset) => {
                    const decision = await authorizeAssetReadDecision(
                        trx,
                        tenant,
                        context,
                        { asset_id: asset.asset_id, client_id: asset.client_id }
                    );
                    return decision.allowed;
                },
                scanLimit: Math.max(limit, 100),
            });

        // Get extension data for each asset if requested
        const assetsWithExtensions = await Promise.all(
            authorizedPage.data.map(async (asset: any): Promise<Asset> => {
                const extensionData = validatedParams.include_extension_data
                    ? await getExtensionData(trx, tenant, asset.asset_id, asset.asset_type)
                    : null;

                return {
                    ...asset,
                    client: {
                        client_id: asset.client_id,
                        client_name: asset.client_name || ''
                    },
                    relationships: [],  // Initialize empty array for relationships
                    ...(extensionData ? {
                        [asset.asset_type]: extensionData
                    } : {})
                };
            })
        );

            const response = {
                assets: assetsWithExtensions,
                total: authorizedPage.total,
                page,
                limit
            };

            return response;
        });
    } catch (error) {
        console.error('Error listing assets:', error);
        throw new Error('Failed to list assets');
    }
});

// Maintenance Schedule Management
export const createMaintenanceSchedule = withAuth(async (user, { tenant }, data: CreateMaintenanceScheduleRequest): Promise<AssetMaintenanceSchedule> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating (maintenance is considered an update operation)
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot create maintenance schedules');
    }

    try {
        // Validate the input data
        const validatedData = validateData(createMaintenanceScheduleSchema, data);

        const schedule = await withTransaction(knex, async (trx: Knex.Transaction) => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, validatedData.asset_id);

            // Insert the schedule
            const [createdSchedule] = await trx('asset_maintenance_schedules')
                .insert({
                    tenant,
                    asset_id: validatedData.asset_id,
                    schedule_name: validatedData.schedule_name,
                    description: validatedData.description || null,
                    maintenance_type: validatedData.maintenance_type,
                    frequency: validatedData.frequency,
                    frequency_interval: validatedData.frequency_interval,
                    schedule_config: validatedData.schedule_config,
                    next_maintenance: validatedData.next_maintenance,
                    created_by: user.user_id
                })
                .returning('*');

            // Create initial notification
            await trx('asset_maintenance_notifications')
                .insert({
                    tenant,
                    schedule_id: createdSchedule.schedule_id,
                    asset_id: createdSchedule.asset_id,
                    notification_type: 'upcoming',
                    notification_date: createdSchedule.next_maintenance,
                    notification_data: {
                        schedule_name: createdSchedule.schedule_name,
                        maintenance_type: createdSchedule.maintenance_type
                    }
                });

            return createdSchedule;
        });

        revalidatePath('/assets');
        revalidatePath(`/assets/${data.asset_id}`);

        // Transform the returned schedule to match the schema (convert Date objects to ISO strings)
        const transformedSchedule = {
            ...schedule,
            next_maintenance: schedule.next_maintenance instanceof Date
                ? schedule.next_maintenance.toISOString()
                : schedule.next_maintenance,
            last_maintenance: schedule.last_maintenance instanceof Date
                ? schedule.last_maintenance.toISOString()
                : schedule.last_maintenance || undefined,
            created_at: schedule.created_at instanceof Date
                ? schedule.created_at.toISOString()
                : schedule.created_at,
            updated_at: schedule.updated_at instanceof Date
                ? schedule.updated_at.toISOString()
                : schedule.updated_at,
            description: schedule.description || undefined
        };

        return validateData(assetMaintenanceScheduleSchema, transformedSchedule) as AssetMaintenanceSchedule;
    } catch (error) {
        console.error('Error creating maintenance schedule:', error);
        throw new Error('Failed to create maintenance schedule');
    }
});

export const updateMaintenanceSchedule = withAuth(async (
    user,
    { tenant },
    schedule_id: string,
    data: UpdateMaintenanceScheduleRequest
): Promise<AssetMaintenanceSchedule> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating (maintenance is considered an update operation)
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot update maintenance schedules');
    }

    try {
        // Validate the update data
        const validatedData = validateData(updateMaintenanceScheduleSchema, data);

        const schedule = await withTransaction(knex, async (trx: Knex.Transaction) => {
            const existingSchedule = await trx('asset_maintenance_schedules')
                .where({ tenant, schedule_id })
                .select('asset_id')
                .first();

            if (!existingSchedule) {
                throw new Error('Maintenance schedule not found');
            }

            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, existingSchedule.asset_id);

            // Update the schedule
            const [updatedSchedule] = await trx('asset_maintenance_schedules')
                .where({ tenant, schedule_id })
                .update({
                    ...validatedData,
                    updated_at: trx.fn.now()
                })
                .returning('*');

            // Update notifications if next_maintenance changed
            if (validatedData.next_maintenance) {
                await trx('asset_maintenance_notifications')
                    .where({
                        tenant,
                        schedule_id,
                        is_sent: false
                    })
                    .update({
                        notification_date: validatedData.next_maintenance,
                        notification_data: trx.raw(`
                            jsonb_set(
                                notification_data,
                                '{schedule_name}',
                                ?::jsonb
                            )
                        `, [JSON.stringify(validatedData.schedule_name || updatedSchedule.schedule_name)])
                    });
            }

            return updatedSchedule;
        });

        revalidatePath('/assets');
        revalidatePath(`/assets/${schedule.asset_id}`);

        // Transform the returned schedule to match the schema (convert Date objects to ISO strings)
        const transformedSchedule = {
            ...schedule,
            next_maintenance: schedule.next_maintenance instanceof Date 
                ? schedule.next_maintenance.toISOString() 
                : schedule.next_maintenance,
            last_maintenance: schedule.last_maintenance instanceof Date 
                ? schedule.last_maintenance.toISOString() 
                : schedule.last_maintenance || undefined,
            created_at: schedule.created_at instanceof Date 
                ? schedule.created_at.toISOString() 
                : schedule.created_at,
            updated_at: schedule.updated_at instanceof Date 
                ? schedule.updated_at.toISOString() 
                : schedule.updated_at,
            description: schedule.description || undefined
        };

        return validateData(assetMaintenanceScheduleSchema, transformedSchedule) as AssetMaintenanceSchedule;
    } catch (error) {
        console.error('Error updating maintenance schedule:', error);
        throw new Error('Failed to update maintenance schedule');
    }
});

export const deleteMaintenanceSchedule = withAuth(async (user, { tenant }, schedule_id: string): Promise<void> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset deletion
    if (!await hasPermission(user, 'asset', 'delete')) {
        throw new Error('Permission denied: Cannot delete maintenance schedules');
    }

    try {
        const [schedule] = await withTransaction(knex, async (trx: Knex.Transaction) => {
            const existingSchedule = await trx('asset_maintenance_schedules')
                .where({ tenant, schedule_id })
                .select('asset_id')
                .first();
            if (!existingSchedule) {
                throw new Error('Maintenance schedule not found');
            }

            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, existingSchedule.asset_id);

            return await trx('asset_maintenance_schedules')
                .where({ tenant, schedule_id })
                .delete()
                .returning(['asset_id']);
        });

        revalidatePath('/assets');
        if (schedule) {
            revalidatePath(`/assets/${schedule.asset_id}`);
        }
    } catch (error) {
        console.error('Error deleting maintenance schedule:', error);
        throw new Error('Failed to delete maintenance schedule');
    }
});

export const recordMaintenanceHistory = withAuth(async (user, { tenant }, data: CreateMaintenanceHistoryRequest): Promise<AssetMaintenanceHistory> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating (maintenance recording is considered an update operation)
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot record maintenance history');
    }

    try {
        // Validate the input data
        const validatedData = validateData(createMaintenanceHistorySchema, data);

        const history = await withTransaction(knex, async (trx: Knex.Transaction) => {
            const schedule = await trx('asset_maintenance_schedules')
                .where({
                    tenant,
                    schedule_id: validatedData.schedule_id
                })
                .select('schedule_id', 'asset_id', 'frequency', 'frequency_interval', 'schedule_name', 'maintenance_type')
                .first();

            if (!schedule) {
                throw new Error('Maintenance schedule not found');
            }

            if (schedule.asset_id !== validatedData.asset_id) {
                throw new Error('Maintenance history schedule does not belong to the provided asset');
            }

            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, schedule.asset_id);

            // Record the maintenance history
            const [createdHistory] = await trx('asset_maintenance_history')
                .insert({
                    tenant,
                    ...validatedData,
                    performed_by: user.user_id
                })
                .returning('*');

            // Update the schedule's last maintenance date and calculate next maintenance
            const [updatedSchedule] = await trx('asset_maintenance_schedules')
                .where({
                    tenant,
                    schedule_id: validatedData.schedule_id
                })
                .update({
                    last_maintenance: validatedData.performed_at,
                    next_maintenance: trx.raw(`
                        CASE frequency
                            WHEN 'daily' THEN ? + INTERVAL '1 day' * frequency_interval
                            WHEN 'weekly' THEN ? + INTERVAL '1 week' * frequency_interval
                            WHEN 'monthly' THEN ? + INTERVAL '1 month' * frequency_interval
                            WHEN 'quarterly' THEN ? + INTERVAL '3 months' * frequency_interval
                            WHEN 'yearly' THEN ? + INTERVAL '1 year' * frequency_interval
                            ELSE ? + INTERVAL '1 day' * frequency_interval
                        END
                    `, Array(6).fill(validatedData.performed_at))
                })
                .returning('*');

            // Create next notification
            await trx('asset_maintenance_notifications')
                .insert({
                    tenant,
                    schedule_id: updatedSchedule.schedule_id,
                    asset_id: updatedSchedule.asset_id,
                    notification_type: 'upcoming',
                    notification_date: updatedSchedule.next_maintenance,
                    notification_data: {
                        schedule_name: updatedSchedule.schedule_name,
                        maintenance_type: updatedSchedule.maintenance_type
                    }
                });

            return createdHistory;
        });

        revalidatePath('/assets');
        revalidatePath(`/assets/${data.asset_id}`);

        return validateData(assetMaintenanceHistorySchema, history) as AssetMaintenanceHistory;
    } catch (error) {
        console.error('Error recording maintenance history:', error);
        throw new Error('Failed to record maintenance history');
    }
});

// Maintenance Schedule Listing
export const getAssetMaintenanceSchedules = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetMaintenanceSchedule[]> => {
    const { knex } = await createTenantKnex();

    if (!await hasPermission(user, 'asset', 'read', knex)) {
        throw new Error('Permission denied: Cannot read asset maintenance schedules');
    }

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetMaintenanceSchedule[]> => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

            const schedules = await trx('asset_maintenance_schedules')
                .where({ tenant, asset_id })
                .orderBy('next_maintenance', 'asc')
                .select('*');

            // Transform Date objects to ISO strings
            return schedules.map(schedule => ({
                ...schedule,
                next_maintenance: schedule.next_maintenance instanceof Date
                    ? schedule.next_maintenance.toISOString()
                    : schedule.next_maintenance,
                last_maintenance: schedule.last_maintenance instanceof Date
                    ? schedule.last_maintenance.toISOString()
                    : schedule.last_maintenance || undefined,
                created_at: schedule.created_at instanceof Date
                    ? schedule.created_at.toISOString()
                    : schedule.created_at,
                updated_at: schedule.updated_at instanceof Date
                    ? schedule.updated_at.toISOString()
                    : schedule.updated_at,
                description: schedule.description || undefined
            })) as AssetMaintenanceSchedule[];
        });
    } catch (error) {
        console.error('Error getting asset maintenance schedules:', error);
        throw new Error('Failed to get asset maintenance schedules');
    }
});

// Reporting Functions
export const getAssetMaintenanceReport = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetMaintenanceReport> => {
    const { knex } = await createTenantKnex();

    if (!await hasPermission(user, 'asset', 'read', knex)) {
        throw new Error('Permission denied: Cannot read asset maintenance reports');
    }

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetMaintenanceReport> => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);
            return fetchAssetMaintenanceReport(trx, tenant, asset_id);
        });
    } catch (error) {
        console.error('Error getting asset maintenance report:', error);
        throw new Error('Failed to get asset maintenance report');
    }
});

export const getAssetHistory = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetHistory[]> => {
    const { knex } = await createTenantKnex();

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetHistory[]> => {
            if (!await hasPermission(user, 'asset', 'read', trx)) {
                throw new Error('Permission denied: Cannot read asset history');
            }

            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

            return fetchAssetHistory(trx, tenant, asset_id);
        });
    } catch (error) {
        console.error('Error getting asset history:', error);
        throw new Error('Failed to get asset history');
    }
});

type RawLinkedTicket = {
    entity_id: string;
    relationship_type?: string | null;
    linked_at: string | Date;
    ticket_id: string | null;
    entered_by?: string | null;
    assigned_to?: string | null;
    assigned_team_id?: string | null;
    client_id?: string | null;
    board_id?: string | null;
    title: string | null;
    status_id: string | null;
    status_name: string | null;
    priority_id: string | null;
    priority_name: string | null;
    updated_at: string | Date | null;
    assigned_first_name?: string | null;
    assigned_last_name?: string | null;
    client_name?: string | null;
};

export const getAssetLinkedTickets = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetTicketSummary[]> => {
    const { knex } = await createTenantKnex();

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetTicketSummary[]> => {
            if (!await hasPermission(user, 'ticket', 'read', trx)) {
                throw new Error('Permission denied: Cannot read linked tickets');
            }

            const { context } = await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

            return fetchAssetLinkedTickets(trx, tenant, asset_id, context);
        });
    } catch (error) {
        console.error('Error getting asset linked tickets:', error);
        throw new Error('Failed to get asset linked tickets');
    }
});

export const getClientMaintenanceSummary = withAuth(async (user, { tenant }, client_id: string): Promise<ClientMaintenanceSummary> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset reading
    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read client maintenance summaries');
    }

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<ClientMaintenanceSummary> => {
            const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
            const authorizedAssetIds = await getAuthorizedAssetIdsForClient(trx, tenant, context, client_id);

            if (authorizedAssetIds.length === 0) {
                const clientAssetCount = await countAssetsForClient(trx, tenant, client_id);
                if (clientAssetCount > 0) {
                    throw new Error('Permission denied: Cannot read client maintenance summaries');
                }
            }

            return getClientMaintenanceSummaryForTenant(trx, tenant, client_id, authorizedAssetIds);
        });
    } catch (error) {
        console.error('Error getting client maintenance summary:', error);
        throw new Error('Failed to get client maintenance summary');
    }
});

async function fetchAssetMaintenanceReport(
    db: Knex | Knex.Transaction,
    tenant: string,
    asset_id: string
): Promise<AssetMaintenanceReport> {
    const asset = await db('assets')
        .where({ tenant, asset_id })
        .first();

    if (!asset) {
        throw new Error('Asset not found');
    }

    const stats = await db('asset_maintenance_schedules')
        .where({ tenant, asset_id })
        .select(
            db.raw('COUNT(*) as total_schedules'),
            db.raw('SUM(CASE WHEN is_active THEN 1 ELSE 0 END) as active_schedules'),
            db.raw(`TO_CHAR(MAX(last_maintenance), 'YYYY-MM-DD"T"HH24:MI:SS"Z"') as last_maintenance`),
            db.raw(`TO_CHAR(MIN(next_maintenance), 'YYYY-MM-DD"T"HH24:MI:SS"Z"') as next_maintenance`)
        )
        .first() as unknown as { total_schedules: string; active_schedules: string; last_maintenance: string | null; next_maintenance: string | null } | undefined;

    const history = await db('asset_maintenance_history')
        .where({ tenant, asset_id })
        .orderBy('performed_at', 'desc');

    const completed = await db('asset_maintenance_history')
        .where({ tenant, asset_id })
        .count('* as count')
        .first();

    const scheduled = await db('asset_maintenance_schedules')
        .where({ tenant, asset_id })
        .sum('frequency_interval as sum')
        .first();

    const upcomingCount = await db('asset_maintenance_notifications')
        .where({ tenant, asset_id, is_sent: false })
        .count('* as count')
        .first()
        .then(result => Number(result?.count || 0));

    const completedCount = completed?.count ? Number(completed.count) : 0;
    const scheduledSum = scheduled?.sum ? Number(scheduled.sum) : 0;
    const compliance_rate = scheduledSum > 0 ? (completedCount / scheduledSum) * 100 : 100;

    const report = {
        asset_id,
        asset_name: asset.name,
        total_schedules: Number(stats?.total_schedules || 0),
        active_schedules: Number(stats?.active_schedules || 0),
        completed_maintenances: completedCount,
        upcoming_maintenances: upcomingCount,
        last_maintenance: stats?.last_maintenance || undefined,
        next_maintenance: stats?.next_maintenance || undefined,
        compliance_rate,
        maintenance_history: history.map((record): AssetMaintenanceHistory => ({
            ...record,
            performed_at: typeof record.performed_at === 'string'
                ? record.performed_at
                : new Date(record.performed_at).toISOString(),
            created_at: typeof record.created_at === 'string'
                ? record.created_at
                : record.created_at instanceof Date
                    ? record.created_at.toISOString()
                    : new Date(record.created_at).toISOString()
        }))
    };

    return validateData(assetMaintenanceReportSchema, report) as AssetMaintenanceReport;
}

async function fetchAssetHistory(
    db: Knex | Knex.Transaction,
    tenant: string,
    asset_id: string
): Promise<AssetHistory[]> {
    const history = await db('asset_history as ah')
        .leftJoin('users as u', function() {
            this.on('ah.changed_by', '=', 'u.user_id')
                .andOn('ah.tenant', '=', 'u.tenant');
        })
        .where({ 'ah.tenant': tenant, 'ah.asset_id': asset_id })
        .select(
            'ah.*',
            db.raw("CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, '')) as changed_by_name")
        )
        .orderBy('ah.changed_at', 'desc');

    return history.map((record): AssetHistory => ({
        tenant: record.tenant,
        history_id: record.history_id,
        asset_id: record.asset_id,
        changed_by: record.changed_by,
        changed_by_name: record.changed_by_name?.trim() || undefined,
        change_type: record.change_type,
        changes: record.changes,
        changed_at: typeof record.changed_at === 'string'
            ? record.changed_at
            : new Date(record.changed_at).toISOString()
    }));
}

async function fetchAssetLinkedTickets(
    db: Knex | Knex.Transaction,
    tenant: string,
    asset_id: string,
    authorizationContext?: AssetReadAuthorizationContext
): Promise<AssetTicketSummary[]> {
    const rows = await db('asset_associations as aa')
        .leftJoin('tickets as t', function(this: Knex.JoinClause) {
            this.on('aa.entity_id', '=', 't.ticket_id')
                .andOn('aa.tenant', '=', 't.tenant');
        })
        .leftJoin('statuses as s', function(this: Knex.JoinClause) {
            this.on('t.status_id', '=', 's.status_id')
                .andOn('t.tenant', '=', 's.tenant');
        })
        .leftJoin('priorities as p', function(this: Knex.JoinClause) {
            this.on('t.priority_id', '=', 'p.priority_id')
                .andOn('t.tenant', '=', 'p.tenant');
        })
        .leftJoin('users as u', function(this: Knex.JoinClause) {
            this.on('t.assigned_to', '=', 'u.user_id')
                .andOn('t.tenant', '=', 'u.tenant');
        })
        .leftJoin('clients as c', function(this: Knex.JoinClause) {
            this.on('t.client_id', '=', 'c.client_id')
                .andOn('t.tenant', '=', 'c.tenant');
        })
        .where({
            'aa.tenant': tenant,
            'aa.asset_id': asset_id,
            'aa.entity_type': 'ticket'
        })
        .orderBy('aa.created_at', 'desc')
        .select<RawLinkedTicket[]>(
            'aa.entity_id',
            'aa.relationship_type',
            'aa.created_at as linked_at',
            't.ticket_id',
            't.entered_by',
            't.assigned_to',
            't.assigned_team_id',
            't.client_id',
            't.board_id',
            't.title',
            't.status_id',
            's.name as status_name',
            't.priority_id',
            'p.priority_name',
            't.updated_at',
            'u.first_name as assigned_first_name',
            'u.last_name as assigned_last_name',
            'c.client_name'
        );

    const rowsAfterIntersection = authorizationContext
        ? (await Promise.all(rows.map(async (row) => {
            if (!row.ticket_id) {
                return null;
            }

            const decision = await authorizationContext.authorizationKernel.authorizeResource({
                subject: authorizationContext.subject,
                resource: { type: 'ticket', action: 'read', id: row.ticket_id },
                record: {
                    id: row.ticket_id,
                    ownerUserId: row.entered_by ?? null,
                    assignedUserIds: row.assigned_to ? [row.assigned_to] : [],
                    clientId: row.client_id ?? null,
                    boardId: row.board_id ?? undefined,
                    teamIds: row.assigned_team_id ? [row.assigned_team_id] : [],
                    statusId: row.status_id,
                },
                requestCache: authorizationContext.requestCache,
                knex: db as Knex.Transaction,
            });

            return decision.allowed ? row : null;
        }))).filter((row): row is RawLinkedTicket => Boolean(row))
        : rows;

    return rowsAfterIntersection.map((row): AssetTicketSummary => {
        const ticketId = row.ticket_id || row.entity_id;
        const assigned_to_name = [row.assigned_first_name, row.assigned_last_name]
            .filter(Boolean)
            .join(' ')
            .trim();

        return {
            ticket_id: ticketId,
            title: row.title || 'Linked ticket unavailable',
            status_id: row.status_id || 'unknown',
            status_name: row.status_name || 'Unknown',
            priority_id: row.priority_id || undefined,
            priority_name: row.priority_name || undefined,
            linked_at: typeof row.linked_at === 'string'
                ? row.linked_at
                : row.linked_at.toISOString(),
            updated_at: row.updated_at
                ? (row.updated_at instanceof Date ? row.updated_at.toISOString() : row.updated_at)
                : undefined,
            client_name: row.client_name || undefined,
            assigned_to_name: assigned_to_name.length > 0 ? assigned_to_name : undefined,
            relationship_type: row.relationship_type || undefined
        };
    });
}

async function fetchAssetDocuments(
    db: Knex | Knex.Transaction,
    tenant: string,
    asset_id: string,
    asset_client_id: string | null,
    limit = 15,
    authorizationContext?: AssetReadAuthorizationContext
): Promise<IDocument[]> {
    const records = await db('documents')
        .join('document_associations', function() {
            this.on('documents.document_id', '=', 'document_associations.document_id')
                .andOn('document_associations.tenant', '=', db.raw('?', [tenant]));
        })
        .leftJoin('users', function() {
            this.on('documents.created_by', '=', 'users.user_id')
                .andOn('users.tenant', '=', db.raw('?', [tenant]));
        })
        .where('documents.tenant', tenant)
        .where('document_associations.entity_id', asset_id)
        .andWhere('document_associations.entity_type', 'asset')
        .orderBy('documents.updated_at', 'desc')
        .limit(limit)
        .select(
            'documents.*',
            db.raw("CONCAT(users.first_name, ' ', users.last_name) as created_by_full_name")
        );

    // The document's client linkage in this view is the asset's owning client
    // (we already filtered to documents associated with this asset). `documents`
    // has no `client_id` column of its own, so without this the kernel always
    // saw clientId=null and portal users from the asset's client failed
    // `same_client`.
    const recordsAfterIntersection = authorizationContext
        ? (await Promise.all(records.map(async (record) => {
            const decision = await authorizationContext.authorizationKernel.authorizeResource({
                subject: authorizationContext.subject,
                resource: { type: 'document', action: 'read', id: record.document_id },
                record: {
                    id: record.document_id,
                    ownerUserId: record.created_by ?? null,
                    clientId: asset_client_id,
                    is_client_visible: record.is_client_visible === true,
                },
                requestCache: authorizationContext.requestCache,
                knex: db as Knex.Transaction,
            });

            return decision.allowed ? record : null;
        }))).filter((record): record is any => Boolean(record))
        : records;

    return recordsAfterIntersection.map((record) => ({
        document_id: record.document_id,
        document_name: record.document_name,
        type_id: record.type_id,
        shared_type_id: record.shared_type_id,
        user_id: record.created_by,
        order_number: record.order_number || 0,
        created_by: record.created_by,
        tenant: record.tenant,
        file_id: record.file_id,
        storage_path: record.storage_path,
        mime_type: record.mime_type,
        file_size: record.file_size,
        created_by_full_name: record.created_by_full_name,
        entered_at: record.entered_at,
        updated_at: record.updated_at
    }));
}

async function countAssetsForClient(
    db: Knex | Knex.Transaction,
    tenant: string,
    client_id: string
): Promise<number> {
    const result = await db('assets')
        .where({ tenant, client_id })
        .count<{ count: string }>('asset_id as count')
        .first();

    return Number(result?.count || 0);
}

async function getClientMaintenanceSummaryForTenant(
    db: Knex | Knex.Transaction,
    tenant: string,
    client_id: string,
    authorizedAssetIds?: string[]
): Promise<ClientMaintenanceSummary> {
    const client = await db('clients')
        .where({ tenant, client_id })
        .first();

    if (!client) {
        throw new Error('Client not found');
    }

    const clientAssetIds = authorizedAssetIds !== undefined
        ? authorizedAssetIds
        : await db('assets')
            .where({ 'assets.tenant': tenant, client_id })
            .pluck<string[]>('asset_id');

    if (clientAssetIds.length === 0) {
        return validateData(clientMaintenanceSummarySchema, {
            client_id,
            client_name: client.client_name,
            total_assets: 0,
            assets_with_maintenance: 0,
            total_schedules: 0,
            overdue_maintenances: 0,
            upcoming_maintenances: 0,
            compliance_rate: 100,
            maintenance_by_type: {},
        }) as ClientMaintenanceSummary;
    }

    const assetStats = await db('assets')
        .where({ 'assets.tenant': tenant, client_id })
        .whereIn('assets.asset_id', clientAssetIds)
        .select(
            db.raw('COUNT(DISTINCT assets.asset_id) as total_assets'),
            db.raw(`
                COUNT(DISTINCT CASE
                    WHEN asset_maintenance_schedules.asset_id IS NOT NULL
                    THEN assets.asset_id
                END) as assets_with_maintenance
            `)
        )
        .leftJoin('asset_maintenance_schedules', function(this: Knex.JoinClause) {
            this.on('assets.asset_id', '=', 'asset_maintenance_schedules.asset_id')
                .andOn('asset_maintenance_schedules.tenant', '=', db.raw('?', [tenant]));
        })
        .first() as unknown as { total_assets: string; assets_with_maintenance: string } | undefined;

    const maintenanceStats = await db('asset_maintenance_schedules')
        .where({ 'asset_maintenance_schedules.tenant': tenant })
        .whereIn('asset_id', clientAssetIds)
        .select(
            db.raw('COUNT(*) as total_schedules'),
            db.raw(`
                COUNT(CASE
                    WHEN next_maintenance < NOW() AND is_active
                    THEN 1
                END) as overdue_maintenances
            `),
            db.raw(`
                COUNT(CASE
                    WHEN next_maintenance > NOW() AND is_active
                    THEN 1
                END) as upcoming_maintenances
            `)
        )
        .first() as unknown as { total_schedules: string; overdue_maintenances: string; upcoming_maintenances: string } | undefined;

    const typeBreakdown = await db('asset_maintenance_schedules')
        .where({ 'asset_maintenance_schedules.tenant': tenant })
        .whereIn('asset_id', clientAssetIds)
        .select('maintenance_type')
        .count('* as count')
        .groupBy('maintenance_type')
        .then(results =>
            results.reduce((acc, { maintenance_type, count }) => ({
                ...acc,
                [maintenance_type]: Number(count)
            }), {} as Record<string, number>)
        );

    const completed = await db('asset_maintenance_history')
        .where({ 'asset_maintenance_history.tenant': tenant })
        .whereIn('asset_id', clientAssetIds)
        .count('* as count')
        .first();

    const scheduled = await db('asset_maintenance_schedules')
        .where({ 'asset_maintenance_schedules.tenant': tenant })
        .whereIn('asset_id', clientAssetIds)
        .sum('frequency_interval as sum')
        .first();

    const completedCount = completed?.count ? Number(completed.count) : 0;
    const scheduledSum = scheduled?.sum ? Number(scheduled.sum) : 0;
    const compliance_rate = scheduledSum > 0 ? (completedCount / scheduledSum) * 100 : 100;

    const summary = {
        client_id,
        client_name: client.client_name,
        total_assets: Number(assetStats?.total_assets || 0),
        assets_with_maintenance: Number(assetStats?.assets_with_maintenance || 0),
        total_schedules: Number(maintenanceStats?.total_schedules || 0),
        overdue_maintenances: Number(maintenanceStats?.overdue_maintenances || 0),
        upcoming_maintenances: Number(maintenanceStats?.upcoming_maintenances || 0),
        compliance_rate,
        maintenance_by_type: typeBreakdown || {}
    };

    return validateData(clientMaintenanceSummarySchema, summary) as ClientMaintenanceSummary;
}

export const getClientMaintenanceSummaries = withAuth(async (user, { tenant }, client_ids: string[]): Promise<Record<string, ClientMaintenanceSummary>> => {
    if (client_ids.length === 0) {
        return {};
    }

    const { knex } = await createTenantKnex();

    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read client maintenance summaries');
    }

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<Record<string, ClientMaintenanceSummary>> => {
            const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);
            const entries = await Promise.all(
                client_ids.map(async (clientId) => {
                    try {
                        const authorizedAssetIds = await getAuthorizedAssetIdsForClient(trx, tenant, context, clientId);
                        if (authorizedAssetIds.length === 0) {
                            const clientAssetCount = await countAssetsForClient(trx, tenant, clientId);
                            if (clientAssetCount > 0) {
                                return null;
                            }
                        }
                        const summary = await getClientMaintenanceSummaryForTenant(trx, tenant, clientId, authorizedAssetIds);
                        return [clientId, summary] as const;
                    } catch (error) {
                        console.error('Failed to load maintenance summary for client', clientId, error);
                        return null;
                    }
                })
            );

            return entries.reduce<Record<string, ClientMaintenanceSummary>>((acc, entry) => {
                if (entry) {
                    acc[entry[0]] = entry[1];
                }
                return acc;
            }, {});
        });
    } catch (error) {
        console.error('Error getting client maintenance summaries:', error);
        throw new Error('Failed to get client maintenance summaries');
    }
});

// Asset Association Functions
// Update only the map callback in listEntityAssets function
export const listEntityAssets = withAuth(async (user, { tenant }, entity_id: string, entity_type: 'ticket' | 'project'): Promise<Asset[]> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset reading
    if (!await hasPermission(user, 'asset', 'read')) {
        throw new Error('Permission denied: Cannot read asset associations');
    }

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<Asset[]> => {
            const context = await createAssetReadAuthorizationContext(trx, tenant, user as AssetAuthUser);

            // Get asset associations
            const associations = await trx('asset_associations')
                .where({
                    tenant,
                    entity_id,
                    entity_type
                })
                .select('asset_id');

            const candidateAssetIds = associations.map((association: { asset_id: string }) => association.asset_id);
            const uniqueAssetIds = Array.from(new Set(candidateAssetIds));
            if (uniqueAssetIds.length === 0) {
                return [];
            }

            const candidateAssets = await trx('assets')
                .where({ tenant })
                .whereIn('asset_id', uniqueAssetIds)
                .select('asset_id', 'client_id') as AssetAuthorizationInput[];

            const decisions = await Promise.all(
                candidateAssets.map((asset) => authorizeAssetReadDecision(trx, tenant, context, asset))
            );

            const authorizedAssetIds = candidateAssets
                .filter((_, index) => decisions[index]?.allowed)
                .map((asset) => asset.asset_id);

            if (authorizedAssetIds.length === 0) {
                return [];
            }

            const assets = await Promise.all(
                authorizedAssetIds.map(async (authorizedAssetId): Promise<Asset> =>
                    getAssetWithExtensions(trx, tenant, authorizedAssetId)
                )
            );

            return assets;
        });
    } catch (error) {
        console.error('Error listing entity assets:', error);
        throw new Error('Failed to list entity assets');
    }
});

export const createAssetAssociation = withAuth(async (user, { tenant }, data: CreateAssetAssociationRequest): Promise<AssetAssociation> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset updating (associations are considered update operations)
    if (!await hasPermission(user, 'asset', 'update')) {
        throw new Error('Permission denied: Cannot create asset associations');
    }

    try {
        // Validate the input data
        const validatedData = validateData(createAssetAssociationSchema, data);

        const association = await withTransaction(knex, async (trx: Knex.Transaction) => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, validatedData.asset_id);

            // Create the association
            const [createdAssociation] = await trx('asset_associations')
                .insert({
                    tenant,
                    ...validatedData,
                    created_by: user.user_id,
                    created_at: trx.fn.now()
                })
                .returning('*');

            return createdAssociation;
        });

        const occurredAt = new Date().toISOString();
        await publishWorkflowEvent({
            eventType: 'ASSET_ASSIGNED',
            payload: buildAssetAssignedPayload({
                assetId: validatedData.asset_id,
                newOwnerType: validatedData.entity_type,
                newOwnerId: validatedData.entity_id,
                assignedAt: occurredAt,
            }),
            ctx: {
                tenantId: tenant,
                occurredAt,
                actor: { actorType: 'USER', actorUserId: user.user_id },
            },
        });

        // Revalidate paths
        revalidatePath('/assets');
        revalidatePath(`/assets/${data.asset_id}`);
        if (data.entity_type === 'ticket') {
            revalidatePath(`/tickets/${data.entity_id}`);
        } else {
            revalidatePath(`/projects/${data.entity_id}`);
        }

        // Convert Date to ISO string for schema validation
        const sanitizedAssociation = {
            ...association,
            created_at: association.created_at instanceof Date
                ? association.created_at.toISOString()
                : association.created_at
        };

        return validateData(assetAssociationSchema, sanitizedAssociation) as AssetAssociation;
    } catch (error) {
        console.error('Error creating asset association:', error);
        throw new Error('Failed to create asset association');
    }
});

export const removeAssetAssociation = withAuth(async (
    user,
    { tenant },
    asset_id: string,
    entity_id: string,
    entity_type: 'ticket' | 'project'
): Promise<void> => {
    const { knex } = await createTenantKnex();

    // Check permission for asset deletion
    if (!await hasPermission(user, 'asset', 'delete')) {
        throw new Error('Permission denied: Cannot remove asset associations');
    }

    try {
        await withTransaction(knex, async (trx: Knex.Transaction) => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

            await trx('asset_associations')
                .where({
                    tenant,
                    asset_id,
                    entity_id,
                    entity_type
                })
                .delete();
        });

        const occurredAt = new Date().toISOString();
        await publishWorkflowEvent({
            eventType: 'ASSET_UNASSIGNED',
            payload: buildAssetUnassignedPayload({
                assetId: asset_id,
                previousOwnerType: entity_type,
                previousOwnerId: entity_id,
                unassignedAt: occurredAt,
                reason: 'manual_detach',
            }),
            ctx: {
                tenantId: tenant,
                occurredAt,
                actor: { actorType: 'USER', actorUserId: user.user_id },
            },
        });

        // Revalidate paths
        revalidatePath('/assets');
        revalidatePath(`/assets/${asset_id}`);
        if (entity_type === 'ticket') {
            revalidatePath(`/tickets/${entity_id}`);
        } else {
            revalidatePath(`/projects/${entity_id}`);
        }
    } catch (error) {
        console.error('Error removing asset association:', error);
        throw new Error('Failed to remove asset association');
    }
});

export const getAssetSummaryMetrics = withAuth(async (user, { tenant }, asset_id: string): Promise<AssetSummaryMetrics> => {
    const { knex } = await createTenantKnex();

    try {
        return await withTransaction(knex, async (trx: Knex.Transaction): Promise<AssetSummaryMetrics> => {
            await createAuthorizedAssetReadContextForUser(trx, tenant, user as AssetAuthUser, asset_id);

            // Get asset info
            const asset = await trx('assets')
                .where({ tenant, asset_id })
                .select(
                    'asset_type',
                    'agent_status',
                    'last_seen_at',
                    'warranty_end_date'
                )
                .first();

            if (!asset) {
                throw new Error('Asset not found');
            }

            // Calculate health status based on agent status and last seen time
            const { health_status, health_reason } = calculateHealthStatus(asset);

            // Count open tickets associated with this asset
            const ticketCountResult = await trx('asset_associations')
                .where('asset_associations.tenant', tenant)
                .where('asset_associations.asset_id', asset_id)
                .where('asset_associations.entity_type', 'ticket')
                .join('tickets', function() {
                    this.on('tickets.tenant', '=', 'asset_associations.tenant')
                        .andOn('tickets.ticket_id', '=', 'asset_associations.entity_id');
                })
                .join('statuses', function() {
                    this.on('statuses.tenant', '=', 'tickets.tenant')
                        .andOn('statuses.status_id', '=', 'tickets.status_id');
                })
                .where('statuses.is_closed', false)
                .count('* as count')
                .first();

            const open_tickets_count = parseInt(String(ticketCountResult?.count || 0), 10);

            // Calculate security status based on asset extension data
            const { security_status, security_issues } = await calculateSecurityStatus(
                trx,
                tenant,
                asset_id,
                asset.asset_type
            );

            // Calculate warranty status
            const { warranty_status, warranty_days_remaining } = calculateWarrantyStatus(
                asset.warranty_end_date
            );

            return {
                health_status,
                health_reason,
                open_tickets_count,
                security_status,
                security_issues,
                warranty_days_remaining,
                warranty_status,
            };
        });
    } catch (error) {
        console.error('Error getting asset summary metrics:', error);
        throw new Error('Failed to get asset summary metrics');
    }
});

/**
 * Calculate health status based on agent status and last seen time
 */
function calculateHealthStatus(asset: {
    agent_status: string | null;
    last_seen_at: string | null;
}): { health_status: HealthStatus; health_reason: string | null } {
    // If no RMM data, return unknown
    if (!asset.agent_status) {
        return { health_status: 'unknown', health_reason: 'No RMM data available' };
    }

    // Check agent status
    if (asset.agent_status === 'offline' || asset.agent_status === 'overdue') {
        const stateLabel = asset.agent_status === 'overdue' ? 'overdue' : 'offline';

        // Check how long it's been offline
        if (asset.last_seen_at) {
            const lastSeen = new Date(asset.last_seen_at);
            const now = new Date();
            const hoursSinceLastSeen = (now.getTime() - lastSeen.getTime()) / (1000 * 60 * 60);

            if (hoursSinceLastSeen > 72) {
                return {
                    health_status: 'critical',
                    health_reason: `Device ${stateLabel} for ${Math.floor(hoursSinceLastSeen / 24)} days`,
                };
            } else if (hoursSinceLastSeen > 24) {
                return {
                    health_status: 'warning',
                    health_reason: `Device ${stateLabel} for ${Math.floor(hoursSinceLastSeen)} hours`,
                };
            }
        }
        return { health_status: 'warning', health_reason: `Device ${stateLabel}` };
    }

    // Agent is online - consider healthy
    return { health_status: 'healthy', health_reason: null };
}

/**
 * Calculate security status based on antivirus and patch status
 */
async function calculateSecurityStatus(
    knex: Knex,
    tenant: string,
    assetId: string,
    assetType: string
): Promise<{ security_status: SecurityStatus; security_issues: string[] }> {
    const issues: string[] = [];

    // Get extension data based on asset type
    let extensionData: {
        antivirus_status?: string;
        antivirus_product?: string;
        pending_patches?: number;
        failed_patches?: number;
    } | null = null;

    if (assetType === 'workstation') {
        extensionData = await knex('workstation_assets')
            .where({ tenant, asset_id: assetId })
            .select('antivirus_status', 'antivirus_product', 'pending_patches', 'failed_patches')
            .first();
    } else if (assetType === 'server') {
        extensionData = await knex('server_assets')
            .where({ tenant, asset_id: assetId })
            .select('antivirus_status', 'antivirus_product', 'pending_patches', 'failed_patches')
            .first();
    }

    if (!extensionData) {
        return { security_status: 'secure', security_issues: [] };
    }

    // Check antivirus status
    if (extensionData.antivirus_status === 'at_risk') {
        issues.push('Antivirus protection at risk');
    } else if (!extensionData.antivirus_product) {
        issues.push('No antivirus detected');
    }

    // Check patch status
    if (extensionData.failed_patches && extensionData.failed_patches > 0) {
        issues.push(`${extensionData.failed_patches} failed patches`);
    }

    if (extensionData.pending_patches && extensionData.pending_patches > 10) {
        issues.push(`${extensionData.pending_patches} pending patches`);
    }

    // Determine security status based on issues
    let security_status: SecurityStatus = 'secure';
    if (issues.length > 0) {
        // Critical if AV is at risk or many failed patches
        if (
            extensionData.antivirus_status === 'at_risk' ||
            (extensionData.failed_patches && extensionData.failed_patches > 5)
        ) {
            security_status = 'critical';
        } else {
            security_status = 'at_risk';
        }
    }

    return { security_status, security_issues: issues };
}

/**
 * Calculate warranty status based on warranty end date
 */
function calculateWarrantyStatus(warrantyEndDate: string | null): {
    warranty_status: WarrantyStatus;
    warranty_days_remaining: number | null;
} {
    if (!warrantyEndDate) {
        return { warranty_status: 'unknown', warranty_days_remaining: null };
    }

    const endDate = new Date(warrantyEndDate);
    const now = new Date();
    const daysRemaining = Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));

    if (daysRemaining < 0) {
        return { warranty_status: 'expired', warranty_days_remaining: daysRemaining };
    } else if (daysRemaining <= 90) {
        return { warranty_status: 'expiring_soon', warranty_days_remaining: daysRemaining };
    } else {
        return { warranty_status: 'active', warranty_days_remaining: daysRemaining };
    }
}
