﻿import { Response } from 'express';
import { Ticket } from '../models/Ticket';
import { AuthRequest } from '../middleware/auth.middleware';
import { AuditLog } from '../models/AuditLog';
import { createTicketSLAAlert, createTicketAssignmentAlert } from '../services/alertService';
import mongoose from 'mongoose';
import { findProjectByIdOrCode } from '../utils/projectHelper';
import { buildTicketWebhookPayload, sendCsmWebhookSafely } from '../services/csmWebhookClient';
import { isCsmEnabled } from '../csm/config';
import { resolveCsmUserNames } from '../csm/directory';

export const getTickets = async (req: AuthRequest, res: Response): Promise<void> => {
  try {
    const { projectId } = req.query;
    const filter: any = {};

    if (projectId) {
      // Check if it's a valid ObjectId string - if so, treat it as an ID directly
      if (mongoose.Types.ObjectId.isValid(projectId as string)) {
        // Query for both the string representation and the ObjectId to handle mixed data types in DB
        const idAsString = projectId as string;
        const idAsObjectId = new mongoose.Types.ObjectId(idAsString);
        filter.projectId = { $in: [idAsString, idAsObjectId] };
      } else {
        // Not a valid ObjectId, assume it's a Project Code
        const project = await findProjectByIdOrCode(projectId as string);
        if (!project) {
          res.status(404).json({
            success: false,
            message: `Project with ID or code "${projectId}" not found`,
          });
          return;
        }

        // Found the project. Use its ID.
        // Again, handle mixed types for the Project's own ID
        const projectIds: any[] = [project._id];

        // If the project's _id is strictly an ObjectId, add string version
        if (project._id instanceof mongoose.Types.ObjectId) {
          projectIds.push(project._id.toString());
        }
        // If it's a string that looks like an ObjectId, add ObjectId version
        else if (typeof project._id === 'string' && mongoose.Types.ObjectId.isValid(project._id)) {
          projectIds.push(new mongoose.Types.ObjectId(project._id));
        }

        filter.projectId = { $in: projectIds };
      }
    }


    // Use native MongoDB query if projectId is a string to avoid ObjectId casting
    let tickets;
    if (filter.projectId && typeof filter.projectId === 'string' && !mongoose.Types.ObjectId.isValid(filter.projectId)) {
      const db = mongoose.connection.db;
      if (!db) throw new Error('Database connection not established');
      const ticketsCollection = db.collection('tickets');
      tickets = await ticketsCollection.find(filter)
        .sort({ createdAt: -1 })
        .toArray();
    } else {
      // Under CSM, reportedBy holds a CSM user id not present in local `users`;
      // populating it would null the field. Return the raw id — the frontend
      // resolves names via the CSM directory (plan §9.5).
      let q = Ticket.find(filter)
        .populate('projectId', 'name code')
        .populate('assetId', 'name code');
      if (!isCsmEnabled()) q = q.populate('reportedBy', 'name email');
      tickets = await q.sort({ createdAt: -1 }).lean();
    }

    res.json({
      success: true,
      data: tickets,
    });
  } catch (error: any) {
    res.status(500).json({
      success: false,
      message: error.message || 'Failed to fetch tickets',
    });
  }
};

export const getTicketById = async (req: AuthRequest, res: Response): Promise<void> => {
  try {
    const { id } = req.params;

    // Support both string and ObjectId for lookup
    const query = {
      $or: [
        { _id: id },
        { _id: mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : null }
      ].filter(f => f._id !== null)
    };

    let tq = Ticket.findOne(query)
      .populate('projectId', 'name code')
      .populate('assetId', 'name code');
    if (!isCsmEnabled()) tq = tq.populate('reportedBy', 'name email');
    const ticket = await tq.lean();

    if (!ticket) {
      res.status(404).json({
        success: false,
        message: 'Ticket not found',
      });
      return;
    }

    res.json({
      success: true,
      data: ticket,
    });
  } catch (error: any) {
    res.status(500).json({
      success: false,
      message: error.message || 'Failed to fetch ticket',
    });
  }
};

export const createTicket = async (req: AuthRequest, res: Response): Promise<void> => {
  try {
    const ticketData = {
      ...req.body,
      reportedBy: req.user?._id || req.body.reportedBy,
    };

    // Generate ticketCode if not provided
    if (!ticketData.ticketCode) {
      const count = await Ticket.countDocuments();
      ticketData.ticketCode = `TKT-${Date.now().toString().substr(-6)}-${count + 1}`;
    }

    const ticket = await Ticket.create(ticketData);

    // Create alert if critical or check SLA
    if (ticket.priority === 'Critical' || ticket.slaDeadline) {
      await createTicketSLAAlert(
        ticket.ticketCode,
        ticket._id.toString(),
        ticket.priority,
        ticket.slaDeadline ? new Date(ticket.slaDeadline) : undefined
      );
    }


    if (req.user) {
      await AuditLog.create({
        userId: req.user._id,
        action: 'CREATE',
        targetCollection: 'tickets',
        targetId: ticket._id.toString(),
        details: `Created ticket: ${ticket.ticketCode}`,
      });
    }

    res.status(201).json({
      success: true,
      data: ticket,
    });
  } catch (error: any) {
    res.status(400).json({
      success: false,
      message: error.message || 'Failed to create ticket',
    });
  }
};

export const updateTicket = async (req: AuthRequest, res: Response): Promise<void> => {
  try {
    const { id } = req.params;
    const updateData = { ...req.body };

    // Remove _id from update data as it is immutable in MongoDB
    delete updateData._id;

    // Support both string and ObjectId for lookup
    const query = {
      $or: [
        { _id: id },
        { _id: mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : null }
      ].filter(f => f._id !== null)
    };

    // Get old ticket to compare changes
    const oldTicket = await Ticket.findOne(query).lean();

    if (!oldTicket) {
      res.status(404).json({
        success: false,
        message: 'Ticket not found',
      });
      return;
    }

    // Handle status changes
    if (updateData.status === 'Resolved' && !updateData.resolvedAt) {
      updateData.resolvedAt = new Date();
    }
    if (updateData.status === 'Closed' && !updateData.closedAt) {
      updateData.closedAt = new Date();
    }

    const ticket = await Ticket.findOneAndUpdate(
      query,
      { ...updateData, updatedAt: new Date() },
      { new: true, runValidators: true }
    );

    if (!ticket) {
      res.status(404).json({
        success: false,
        message: 'Ticket not found',
      });
      return;
    }

    // Create alert if ticket was assigned to someone
    if (updateData.assignedTo && updateData.assignedTo !== oldTicket.assignedTo) {
      try {
        await createTicketAssignmentAlert(
          ticket.ticketCode,
          ticket._id.toString(),
          updateData.assignedTo,
          ticket.priority
        );
      } catch (alertError) {
        console.error('Failed to create assignment alert:', alertError);
        // Don't fail the update if alert creation fails
      }
    }

    // Create alert if SLA deadline is approaching or overdue
    if (ticket.slaDeadline && ticket.status !== 'Resolved' && ticket.status !== 'Closed') {
      try {
        const slaDeadline = ticket.slaDeadline instanceof Date 
          ? ticket.slaDeadline 
          : new Date(ticket.slaDeadline);
        
        await createTicketSLAAlert(
          ticket.ticketCode,
          ticket._id.toString(),
          ticket.priority,
          slaDeadline,
          ticket.assignedTo ? (typeof ticket.assignedTo === 'string' 
            ? (mongoose.Types.ObjectId.isValid(ticket.assignedTo) ? new mongoose.Types.ObjectId(ticket.assignedTo) : undefined)
            : ticket.assignedTo) : undefined
        );
      } catch (alertError) {
        console.error('Failed to create SLA alert:', alertError);
        // Don't fail the update if alert creation fails
      }
    }

    const changedFields = ['status', 'assignedTo', 'priority', 'slaDeadline'].filter((field) => {
      const before = (oldTicket as any)?.[field];
      const after = (ticket as any)?.[field];
      return String(before ?? '') !== String(after ?? '');
    });
    if (changedFields.length > 0) {
      await sendCsmWebhookSafely(buildTicketWebhookPayload(ticket, oldTicket, changedFields));
    }

    if (req.user) {
      await AuditLog.create({
        userId: req.user._id,
        action: 'UPDATE',
        targetCollection: 'tickets',
        targetId: ticket._id.toString(),
        details: `Updated ticket: ${ticket.ticketCode}`,
      });
    }

    res.json({
      success: true,
      data: ticket,
    });
  } catch (error: any) {
    res.status(400).json({
      success: false,
      message: error.message || 'Failed to update ticket',
    });
  }
};

export const deleteTicket = async (req: AuthRequest, res: Response): Promise<void> => {
  try {
    const { id } = req.params;

    // Support both string and ObjectId for lookup
    const query = {
      $or: [
        { _id: id },
        { _id: mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : null }
      ].filter(f => f._id !== null)
    };

    const ticket = await Ticket.findOneAndDelete(query);

    if (!ticket) {
      res.status(404).json({
        success: false,
        message: 'Ticket not found',
      });
      return;
    }


    if (req.user) {
      await AuditLog.create({
        userId: req.user._id,
        action: 'DELETE',
        targetCollection: 'tickets',
        targetId: id,
        details: `Deleted ticket: ${ticket.ticketCode}`,
      });
    }

    res.json({
      success: true,
      message: 'Ticket deleted successfully',
    });
  } catch (error: any) {
    res.status(500).json({
      success: false,
      message: error.message || 'Failed to delete ticket',
    });
  }
};

export const getTicketActivities = async (req: AuthRequest, res: Response): Promise<void> => {
  try {
    const { id } = req.params;

    // Fetch audit logs related to this ticket
    // Support both string ID and ObjectId matching
    const orConditions: any[] = [{ targetId: id }];

    // Only attempt to cast to ObjectId if it is valid, to avoid "input must be a 24 character hex string" error
    if (mongoose.Types.ObjectId.isValid(id)) {
      orConditions.push({ targetId: new mongoose.Types.ObjectId(id).toString() });
    }

    const query = {
      targetCollection: 'tickets',
      $or: orConditions
    };

    // Under CSM, log.userId is a CSM user id (not in local `users`) — resolve
    // names via the CSM directory instead of a local populate (plan §9.5).
    let activities: any[];
    if (isCsmEnabled()) {
      const logs = await AuditLog.find(query).sort({ timestamp: -1 }).lean();
      const nameMap = await resolveCsmUserNames(logs.map((l: any) => l.userId));
      activities = logs.map((log: any) => {
        const u = nameMap.get(String(log.userId));
        return {
          _id: log._id,
          ticketId: id,
          action: log.action,
          performedBy: u ? (u.name || u.email) : 'Unknown',
          timestamp: log.timestamp,
          details: log.details,
        };
      });
    } else {
      const logs = await AuditLog.find(query)
        .sort({ timestamp: -1 })
        .populate('userId', 'name email role')
        .lean();
      activities = logs.map((log: any) => ({
        _id: log._id,
        ticketId: id,
        action: log.action,
        performedBy: log.userId ? (log.userId.name || log.userId.email) : 'Unknown',
        timestamp: log.timestamp,
        details: log.details,
      }));
    }

    res.json({
      success: true,
      data: activities
    });
  } catch (error: any) {
    res.status(500).json({
      success: false,
      message: error.message || 'Failed to fetch ticket activities'
    });
  }
};


