import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import mongoose from 'mongoose';
import { Project } from '../models/Project';
import { Asset } from '../models/Asset';
import { Contract } from '../models/Contract';
import { Ticket } from '../models/Ticket';
import { WorkOrder } from '../models/WorkOrder';
import { User } from '../models/User';
import { Company } from '../models/Company';
import { IntegrationIdempotency } from '../models/IntegrationIdempotency';
import { findProjectByIdOrCode } from '../utils/projectHelper';

type ModelName = 'project' | 'asset' | 'contract' | 'ticket' | 'work order';

function sanitizeUpdate(body: Record<string, unknown>): Record<string, unknown> {
  const update = { ...body };
  delete update._id;
  delete update.__v;
  delete update.createdAt;
  delete update.updatedAt;
  delete update.idempotencyKey;
  return update;
}

function buildTextSearch(q: unknown, fields: string[]): Record<string, unknown>[] {
  const text = typeof q === 'string' ? q.trim() : '';
  if (!text) return [];
  const regex = new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
  return fields.map((field) => ({ [field]: regex }));
}

function objectIdOrStringQuery(id: string): Record<string, unknown> {
  const ids: unknown[] = [id];
  if (mongoose.Types.ObjectId.isValid(id)) ids.push(new mongoose.Types.ObjectId(id));
  return { _id: { $in: ids } };
}

async function resolveProjectId(projectId: unknown): Promise<unknown> {
  if (!projectId) return undefined;
  const raw = String(projectId);
  // Resolve project code → actual id when the param isn't already an id-shaped value.
  let id = raw;
  if (!mongoose.Types.ObjectId.isValid(raw)) {
    const project = await findProjectByIdOrCode(raw);
    if (project?._id) id = String(project._id);
  }
  // projectId references are stored as strings (Mixed _id) but some legacy rows use ObjectId,
  // so match both forms to avoid silently filtering everything out.
  const candidates: unknown[] = [id];
  if (mongoose.Types.ObjectId.isValid(id)) candidates.push(new mongoose.Types.ObjectId(id));
  return { $in: candidates };
}

function sendNotFound(res: Response, model: ModelName): void {
  res.status(404).json({ success: false, message: `${model} not found` });
}

function getIdempotencyKey(req: Request): string {
  return String(req.headers['idempotency-key'] || req.body?.idempotencyKey || '').trim();
}

async function replayIdempotentResponse(req: Request, res: Response): Promise<boolean> {
  const key = getIdempotencyKey(req);
  if (!key) return false;
  const existing = await IntegrationIdempotency.findOne({ key }).lean();
  if (!existing) return false;
  if (existing.method !== req.method || existing.path !== req.originalUrl) {
    res.status(409).json({ success: false, message: 'Idempotency key was already used for another request' });
    return true;
  }
  res.setHeader('X-Idempotent-Replay', 'true');
  res.status(existing.statusCode).json(existing.responsePayload);
  return true;
}

async function sendIdempotentResponse(
  req: Request,
  res: Response,
  statusCode: number,
  payload: Record<string, unknown>,
  resourceType?: string,
  resourceId?: string
): Promise<void> {
  const key = getIdempotencyKey(req);
  if (key) {
    await IntegrationIdempotency.updateOne(
      { key },
      {
        $setOnInsert: {
          key,
          method: req.method,
          path: req.originalUrl,
          statusCode,
          responsePayload: payload,
          resourceType,
          resourceId,
        },
      },
      { upsert: true }
    ).catch(() => undefined);
  }
  res.status(statusCode).json(payload);
}
export const listIntegrationProjects = async (req: Request, res: Response): Promise<void> => {
  try {
    const filter: Record<string, unknown> = {};
    const search = buildTextSearch(req.query.q || req.query.search, ['name', 'code']);
    if (search.length) filter.$or = search;
    if (req.query.status) filter.status = String(req.query.status);
    const limit = Math.min(Number(req.query.limit) || 200, 500);
    const skip = Math.max(Number(req.query.skip) || 0, 0);
    const projects = await Project.find(filter).sort({ updatedAt: -1, createdAt: -1 }).skip(skip).limit(limit).lean();
    const total = await Project.countDocuments(filter);
    res.json({ success: true, data: projects, total });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch projects' });
  }
};

export const getIntegrationProjectById = async (req: Request, res: Response): Promise<void> => {
  try {
    const project = await findProjectByIdOrCode(req.params.id);
    if (!project) return sendNotFound(res, 'project');
    await sendIdempotentResponse(req, res, 200, { success: true, data: project }, 'project', String((project as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch project' });
  }
};

export const createIntegrationProject = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const project = await Project.create(sanitizeUpdate(req.body || {}));
    await sendIdempotentResponse(req, res, 201, { success: true, data: project }, 'project', String((project as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to create project' });
  }
};

export const updateIntegrationProject = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const project = await Project.findOneAndUpdate(objectIdOrStringQuery(req.params.id), { $set: sanitizeUpdate(req.body || {}) }, { new: true, runValidators: true });
    if (!project) return sendNotFound(res, 'project');
    await sendIdempotentResponse(req, res, 200, { success: true, data: project }, 'project', String((project as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to update project' });
  }
};

export const deleteIntegrationProject = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const project = await Project.findOneAndDelete(objectIdOrStringQuery(req.params.id));
    if (!project) return sendNotFound(res, 'project');
    await sendIdempotentResponse(req, res, 200, { success: true, message: 'Project deleted successfully' }, 'project', String((project as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to delete project' });
  }
};

export const listIntegrationAssets = async (req: Request, res: Response): Promise<void> => {
  try {
    const filter: Record<string, unknown> = {};
    const projectId = await resolveProjectId(req.query.projectId);
    if (projectId) filter.projectId = projectId;
    if (req.query.status) filter.status = String(req.query.status);
    const search = buildTextSearch(req.query.q || req.query.search, ['name', 'code', 'serialNumber', 'manufacturer', 'productModel']);
    if (search.length) filter.$or = search;
    const limit = Math.min(Number(req.query.limit) || 200, 500);
    const skip = Math.max(Number(req.query.skip) || 0, 0);
    const assets = await Asset.find(filter).sort({ updatedAt: -1, createdAt: -1 }).skip(skip).limit(limit).lean();
    const total = await Asset.countDocuments(filter);
    res.json({ success: true, data: assets, total });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch assets' });
  }
};

export const getIntegrationAssetById = async (req: Request, res: Response): Promise<void> => {
  try {
    const asset = await Asset.findOne(objectIdOrStringQuery(req.params.id)).lean();
    if (!asset) return sendNotFound(res, 'asset');
    await sendIdempotentResponse(req, res, 200, { success: true, data: asset }, 'asset', String((asset as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch asset' });
  }
};

export const createIntegrationAsset = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const asset = await Asset.create(sanitizeUpdate(req.body || {}));
    await sendIdempotentResponse(req, res, 201, { success: true, data: asset }, 'asset', String((asset as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to create asset' });
  }
};

export const updateIntegrationAsset = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const asset = await Asset.findOneAndUpdate(objectIdOrStringQuery(req.params.id), { $set: sanitizeUpdate(req.body || {}) }, { new: true, runValidators: true });
    if (!asset) return sendNotFound(res, 'asset');
    await sendIdempotentResponse(req, res, 200, { success: true, data: asset }, 'asset', String((asset as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to update asset' });
  }
};

export const deleteIntegrationAsset = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const asset = await Asset.findOneAndDelete(objectIdOrStringQuery(req.params.id));
    if (!asset) return sendNotFound(res, 'asset');
    await sendIdempotentResponse(req, res, 200, { success: true, message: 'Asset deleted successfully' }, 'asset', String((asset as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to delete asset' });
  }
};

export const listIntegrationContracts = async (req: Request, res: Response): Promise<void> => {
  try {
    const filter: Record<string, unknown> = {};
    const projectId = await resolveProjectId(req.query.projectId);
    if (projectId) filter.projectId = projectId;
    const contracts = await Contract.find(filter).sort({ updatedAt: -1, createdAt: -1 }).lean();
    const enriched = await enrichContractsForDisplay(contracts);
    res.json({ success: true, data: enriched, total: enriched.length });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch contracts' });
  }
};

export const createIntegrationContract = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const contract = await Contract.create(sanitizeUpdate(req.body || {}));
    await sendIdempotentResponse(req, res, 201, { success: true, data: contract }, 'contract', String((contract as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to create contract' });
  }
};

export const updateIntegrationContract = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const contract = await Contract.findOneAndUpdate(objectIdOrStringQuery(req.params.id), { $set: sanitizeUpdate(req.body || {}) }, { new: true, runValidators: true });
    if (!contract) return sendNotFound(res, 'contract');
    await sendIdempotentResponse(req, res, 200, { success: true, data: contract }, 'contract', String((contract as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to update contract' });
  }
};

export const deleteIntegrationContract = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const contract = await Contract.findOneAndDelete(objectIdOrStringQuery(req.params.id));
    if (!contract) return sendNotFound(res, 'contract');
    await sendIdempotentResponse(req, res, 200, { success: true, message: 'Contract deleted successfully' }, 'contract', String((contract as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to delete contract' });
  }
};

/** Tách 1 tham chiếu người dùng (ID đơn, CSV, hoặc mảng) thành danh sách ID chuỗi. */
function extractUserIds(ref: unknown): string[] {
  if (ref === undefined || ref === null || ref === '') return [];
  if (Array.isArray(ref)) return ref.map((r) => String(r).trim()).filter(Boolean);
  if (typeof ref === 'object') {
    const id = (ref as any)._id;
    return id ? [String(id)] : [];
  }
  return String(ref)
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean);
}

/**
 * Bổ sung các trường hiển thị (customer/assignee/reportedByName) cho ticket trước khi
 * trả về cho CSM. Dữ liệu thô (assignedTo/reportedBy/projectId) được giữ nguyên để
 * không phá vỡ luồng chỉnh sửa/đồng bộ phía CSM.
 */
async function enrichTicketsForDisplay(tickets: any[]): Promise<any[]> {
  if (!tickets.length) return tickets;

  const userIds = new Set<string>();
  const projectIds = new Set<string>();
  for (const t of tickets) {
    for (const id of extractUserIds(t.assignedTo)) userIds.add(id);
    for (const id of extractUserIds(t.reportedBy)) userIds.add(id);
    if (t.projectId) projectIds.add(String(t.projectId));
  }

  const [users, projects] = await Promise.all([
    userIds.size ? User.find({ _id: { $in: [...userIds] } }).select('name email').lean() : Promise.resolve([]),
    projectIds.size ? Project.find({ _id: { $in: [...projectIds] } }).select('name companyId').lean() : Promise.resolve([]),
  ]);

  const companyIds = new Set<string>();
  for (const p of projects as any[]) if (p.companyId) companyIds.add(String(p.companyId));
  const companies = companyIds.size
    ? await Company.find({ _id: { $in: [...companyIds] } }).select('name').lean()
    : [];

  const userMap = new Map<string, any>((users as any[]).map((u) => [String(u._id), u]));
  const companyMap = new Map<string, any>((companies as any[]).map((c) => [String(c._id), c]));
  const projectMap = new Map<string, any>((projects as any[]).map((p) => [String(p._id), p]));

  const resolveUsers = (ref: unknown): string =>
    extractUserIds(ref)
      .map((id) => userMap.get(id)?.name || userMap.get(id)?.email || id)
      .join(', ');

  return tickets.map((t) => {
    const project = t.projectId ? projectMap.get(String(t.projectId)) : undefined;
    const company = project?.companyId ? companyMap.get(String(project.companyId)) : undefined;
    const assignee = resolveUsers(t.assignedTo);
    return {
      ...t,
      assignee: assignee || undefined,
      reportedByName: resolveUsers(t.reportedBy) || undefined,
      customer: t.sourceCustomerName || company?.name || project?.name || t.customer || undefined,
      projectName: project?.name || t.projectName || undefined,
    };
  });
}

/**
 * Bổ sung tên kỹ thuật viên & tên dự án cho work order trước khi trả về CSM.
 * Giữ nguyên các trường ID thô (assignedTechnicianId/projectId).
 */
async function enrichWorkOrdersForDisplay(workOrders: any[]): Promise<any[]> {
  if (!workOrders.length) return workOrders;

  const userIds = new Set<string>();
  const projectIds = new Set<string>();
  for (const w of workOrders) {
    for (const id of extractUserIds(w.assignedTechnicianId)) userIds.add(id);
    for (const id of extractUserIds(w.assignedTo)) userIds.add(id);
    if (w.projectId) projectIds.add(String(w.projectId));
  }

  const [users, projects] = await Promise.all([
    userIds.size ? User.find({ _id: { $in: [...userIds] } }).select('name email').lean() : Promise.resolve([]),
    projectIds.size ? Project.find({ _id: { $in: [...projectIds] } }).select('name').lean() : Promise.resolve([]),
  ]);

  const userMap = new Map<string, any>((users as any[]).map((u) => [String(u._id), u]));
  const projectMap = new Map<string, any>((projects as any[]).map((p) => [String(p._id), p]));

  const resolveUsers = (ref: unknown): string =>
    extractUserIds(ref)
      .map((id) => userMap.get(id)?.name || userMap.get(id)?.email || id)
      .join(', ');

  return workOrders.map((w) => ({
    ...w,
    assignedTo: resolveUsers(w.assignedTechnicianId) || resolveUsers(w.assignedTo) || w.assignedTo || undefined,
    project: (w.projectId ? projectMap.get(String(w.projectId))?.name : undefined) || w.project || undefined,
  }));
}

/**
 * Bổ sung tên dự án (và tên công ty/khách hàng của dự án) cho hợp đồng trước khi
 * trả về CSM. Giữ nguyên projectId thô.
 */
async function enrichContractsForDisplay(contracts: any[]): Promise<any[]> {
  if (!contracts.length) return contracts;

  const projectIds = new Set<string>();
  for (const c of contracts) if (c.projectId) projectIds.add(String(c.projectId));

  const projects = projectIds.size
    ? await Project.find({ _id: { $in: [...projectIds] } }).select('name code companyId').lean()
    : [];

  const companyIds = new Set<string>();
  for (const p of projects as any[]) if (p.companyId) companyIds.add(String(p.companyId));
  const companies = companyIds.size
    ? await Company.find({ _id: { $in: [...companyIds] } }).select('name').lean()
    : [];

  const projectMap = new Map<string, any>((projects as any[]).map((p) => [String(p._id), p]));
  const companyMap = new Map<string, any>((companies as any[]).map((c) => [String(c._id), c]));

  return contracts.map((c) => {
    const project = c.projectId ? projectMap.get(String(c.projectId)) : undefined;
    const company = project?.companyId ? companyMap.get(String(project.companyId)) : undefined;
    return {
      ...c,
      projectName: project?.name || c.projectName || undefined,
      projectCode: project?.code || c.projectCode || undefined,
      customerName: company?.name || c.customerName || undefined,
    };
  });
}

export const listIntegrationTickets = async (req: Request, res: Response): Promise<void> => {
  try {
    const filter: Record<string, unknown> = {};
    const projectId = await resolveProjectId(req.query.projectId);
    if (projectId) filter.projectId = projectId;
    if (req.query.status) filter.status = String(req.query.status);
    const tickets = await Ticket.find(filter).sort({ updatedAt: -1, createdAt: -1 }).lean();
    const enriched = await enrichTicketsForDisplay(tickets);
    res.json({ success: true, data: enriched, total: enriched.length });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch tickets' });
  }
};


export const getIntegrationTicketById = async (req: Request, res: Response): Promise<void> => {
  try {
    const ticket = await Ticket.findOne(objectIdOrStringQuery(req.params.id)).lean();
    if (!ticket) return sendNotFound(res, 'ticket');
    const [enriched] = await enrichTicketsForDisplay([ticket]);
    res.json({ success: true, data: enriched });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch ticket' });
  }
};
export const createIntegrationTicket = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const body = sanitizeUpdate(req.body || {});
    if (!body.ticketCode) {
      const count = await Ticket.countDocuments();
      body.ticketCode = `TKT-${Date.now().toString().slice(-6)}-${count + 1}`;
    }
    const ticket = await Ticket.create(body);
    await sendIdempotentResponse(req, res, 201, { success: true, data: ticket }, 'ticket', String((ticket as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to create ticket' });
  }
};

export const updateIntegrationTicket = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const ticket = await Ticket.findOneAndUpdate(objectIdOrStringQuery(req.params.id), { $set: sanitizeUpdate(req.body || {}) }, { new: true, runValidators: true });
    if (!ticket) return sendNotFound(res, 'ticket');
    await sendIdempotentResponse(req, res, 200, { success: true, data: ticket }, 'ticket', String((ticket as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to update ticket' });
  }
};

export const deleteIntegrationTicket = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const ticket = await Ticket.findOneAndDelete(objectIdOrStringQuery(req.params.id));
    if (!ticket) return sendNotFound(res, 'ticket');
    await sendIdempotentResponse(req, res, 200, { success: true, message: 'Ticket deleted successfully' }, 'ticket', String((ticket as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to delete ticket' });
  }
};

export const listIntegrationWorkOrders = async (req: Request, res: Response): Promise<void> => {
  try {
    const filter: Record<string, unknown> = {};
    const projectId = await resolveProjectId(req.query.projectId);
    if (projectId) filter.projectId = projectId;
    if (req.query.status) filter.status = String(req.query.status);
    if (req.query.ticketId) filter.ticketId = String(req.query.ticketId);
    const workOrders = await WorkOrder.find(filter).sort({ updatedAt: -1, createdAt: -1 }).lean();
    const enriched = await enrichWorkOrdersForDisplay(workOrders);
    res.json({ success: true, data: enriched, total: enriched.length });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to fetch work orders' });
  }
};

export const createIntegrationWorkOrder = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const body = sanitizeUpdate(req.body || {});
    if (!body.woCode) {
      const count = await WorkOrder.countDocuments();
      body.woCode = `WO-${Date.now().toString().slice(-6)}-${count + 1}`;
    }
    const workOrder = await WorkOrder.create(body);
    await sendIdempotentResponse(req, res, 201, { success: true, data: workOrder }, 'work order', String((workOrder as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to create work order' });
  }
};

export const updateIntegrationWorkOrder = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const workOrder = await WorkOrder.findOneAndUpdate(objectIdOrStringQuery(req.params.id), { $set: sanitizeUpdate(req.body || {}) }, { new: true, runValidators: true });
    if (!workOrder) return sendNotFound(res, 'work order');
    await sendIdempotentResponse(req, res, 200, { success: true, data: workOrder }, 'work order', String((workOrder as any)._id));
  } catch (error: any) {
    res.status(400).json({ success: false, message: error.message || 'Failed to update work order' });
  }
};

export const deleteIntegrationWorkOrder = async (req: Request, res: Response): Promise<void> => {
  try {
    if (await replayIdempotentResponse(req, res)) return;
    const workOrder = await WorkOrder.findOneAndDelete(objectIdOrStringQuery(req.params.id));
    if (!workOrder) return sendNotFound(res, 'work order');
    await sendIdempotentResponse(req, res, 200, { success: true, message: 'Work order deleted successfully' }, 'work order', String((workOrder as any)._id));
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to delete work order' });
  }
};

export const uploadIntegrationContractFile = async (req: Request, res: Response): Promise<void> => {
  try {
    const contract = await Contract.findOne(objectIdOrStringQuery(req.params.id));
    if (!contract) return sendNotFound(res, 'contract');

    const fileName = String(req.body?.fileName || req.body?.originalName || '').trim();
    const contentBase64 = String(req.body?.contentBase64 || '');
    if (!fileName || !contentBase64) {
      res.status(400).json({ success: false, message: 'Missing fileName or contentBase64' });
      return;
    }

    const buffer = Buffer.from(contentBase64, 'base64');
    const dir = path.join(process.cwd(), 'uploads', 'contracts');
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
    const safeExt = path.extname(fileName).replace(/[^a-zA-Z0-9.]/g, '') || '.bin';
    const storedName = `contract-${uniqueSuffix}${safeExt}`;
    fs.writeFileSync(path.join(dir, storedName), buffer);

    const existingFiles = Array.isArray((contract as any).files) ? (contract as any).files : [];
    const version = existingFiles.length + 1;
    const fileEntry = {
      filename: storedName,
      originalName: fileName,
      url: `/uploads/contracts/${storedName}`,
      version,
      uploadedAt: new Date(),
    };
    (contract as any).files = [...existingFiles, fileEntry];
    if (!(contract as any).documentUrl) (contract as any).documentUrl = fileEntry.url;
    await contract.save();
    res.status(201).json({ success: true, data: { file: fileEntry, files: (contract as any).files } });
  } catch (error: any) {
    res.status(500).json({ success: false, message: error.message || 'Failed to upload contract file' });
  }
};

