import crypto from 'crypto';
import mongoose, { Types } from 'mongoose';
import ClinicalRecord from '../models/ClinicalRecord';
import DentalRecord from '../models/DentalRecord';
import Doctor from '../models/Doctor';
import Patient, { IPatient } from '../models/Patient';
import User, { IUser } from '../models/User';
import { isBlobUrl, sanitizeUrlArray } from '../utils/mediaUrlGuard';
import EmailService from './EmailService';

interface CreatePatientWithUserData {
  firstName: string;
  lastName: string;
  email: string;
  phone?: string;
  gender?: string;
  dateOfBirth?: Date;
  address?: string;
  medicalHistory?: string;
  nationalId?: string;
  insurance?: {
    provider?: string;
    policyNumber?: string;
    expirationDate?: Date;
  };
  doctorId: string;
}

interface AddClinicalRecordData {
  patientId: string;
  reasonForConsultation?: string;
  chiefComplaint?: string;
  [key: string]: any;
}

interface AddDentalRecordData {
  patientId: string;
  treatmentPlan?: string;
  [key: string]: any;
}

interface AddTestResultData {
  patientId: string;
  date?: Date | string;
  type?: string;
  name?: string;
  result?: string;
  fileUrl?: string;
  notes?: string;
}

interface AddDocumentData {
  patientId: string;
  date?: Date | string;
  name?: string;
  type?: string;
  url?: string;
  category?: string;
}

export class PatientService {
  private toObjectId(id: string | mongoose.Types.ObjectId): mongoose.Types.ObjectId {
    if (id instanceof mongoose.Types.ObjectId) {
      return id;
    }
    if (!mongoose.Types.ObjectId.isValid(id)) {
      throw new Error('Invalid ObjectId');
    }
    return new mongoose.Types.ObjectId(id);
  }

  private toOptionalObjectId(
    id?: string | mongoose.Types.ObjectId
  ): mongoose.Types.ObjectId | undefined {
    if (id === undefined) {
      return undefined;
    }
    return this.toObjectId(id);
  }

  /**
   * Get doctor ID from user ID
   */
  public async getDoctorIdFromUserId(userId: string): Promise<string> {
    const doctor = await Doctor.findOne({ userId });
    if (!doctor) {
      throw new Error('You must be a doctor to access patients');
    }
    return doctor._id.toString();
  }

  /**
   * Get all doctor IDs accessible to this user's team (owner + teammates).
   */
  private async getAccessibleDoctorIdsFromUserId(userId: string): Promise<string[]> {
    const user = await User.findById(userId).select('_id ownerId');
    if (!user) {
      throw new Error('User not found');
    }

    const ownerId = user.ownerId || user._id;

    const teamUsers = await User.find({
      $or: [{ _id: ownerId }, { ownerId }]
    }).select('_id');

    const doctorProfiles = await Doctor.find({
      userId: { $in: teamUsers.map((u) => u._id) }
    }).select('_id');

    if (!doctorProfiles.length) {
      throw new Error('You must be a doctor to access patients');
    }

    return doctorProfiles.map((doctor) => doctor._id.toString());
  }

  private async assertDoctorOwnsPatient(patientId: string, userId: string) {
    const doctorId = await this.getDoctorIdFromUserId(userId);
    const patient = await Patient.findById(patientId);

    if (!patient) {
      throw new Error('Patient not found');
    }

    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    return patient;
  }

  async getAllPatients(page: number = 1, limit: number = 20, search: string = '', userId: string): Promise<{ data: IPatient[], total: number }> {
    // Get all team doctor IDs (owner + teammates) for shared patient visibility.
    const accessibleDoctorIds = await this.getAccessibleDoctorIdsFromUserId(userId);
    const skip = (page - 1) * limit;

    // Using aggregation to lookup user details and filter/paginate
    const pipeline: any[] = [
      {
        $lookup: {
          from: 'users',
          localField: 'userId',
          foreignField: '_id',
          as: 'userDetails'
        }
      },
      {
        $unwind: '$userDetails'
      },
      {
        $lookup: {
          from: 'doctors',
          localField: 'doctorId',
          foreignField: '_id',
          as: 'doctorDetails'
        }
      },
      {
        $unwind: {
          path: '$doctorDetails',
          preserveNullAndEmptyArrays: true
        }
      }
    ];

    // Filter by doctorId (Mandatory)
    // Need to convert string to ObjectId for aggregation match if stored as ObjectId
    pipeline.push({
      $match: {
        doctorId: { $in: accessibleDoctorIds.map((id) => this.toObjectId(id)) }
      }
    });

    if (search) {
      const searchRegex = new RegExp(search, 'i');
      pipeline.push({
        $match: {
          $or: [
            { 'userDetails.firstName': searchRegex },
            { 'userDetails.lastName': searchRegex },
            { 'userDetails.email': searchRegex },
            { 'userDetails.phone': searchRegex }
          ]
        }
      });
    }

    const countPipeline = [...pipeline, { $count: 'total' }];
    const dataPipeline = [...pipeline, { $sort: { createdAt: -1 } }, { $skip: skip }, { $limit: limit }];

    const [totalResult, dataResult] = await Promise.all([
      Patient.aggregate(countPipeline),
      Patient.aggregate(dataPipeline)
    ]);

    const total = totalResult.length > 0 ? totalResult[0].total : 0;

    const data = dataResult.map(item => {
      const { userDetails, doctorDetails, ...rest } = item;
      return { ...rest, userId: userDetails, doctorId: doctorDetails || null };
    });

    return { data: data as any, total };
  }

  async getPatientById(id: string, userId: string): Promise<IPatient | null> {
    // Get doctor ID from user ID
    const doctorId = await this.getDoctorIdFromUserId(userId);

    const patient = await Patient.findById(id)
      .populate('userId')
      .populate('doctorId')
      .populate({
        path: 'clinicalRecords',
        populate: { path: 'doctorId', populate: { path: 'userId', select: 'firstName lastName profileImage clinic' } }
      })
      .populate({
        path: 'dentalRecords',
        populate: { path: 'doctorId', populate: { path: 'userId', select: 'firstName lastName profileImage clinic' } }
      })
      .exec();

    if (!patient) {
      return null;
    }

    // Determine the ID of the doctor assigned to the patient
    // Handle cases where population might have happened or not
    const patientDoctorId = (patient.doctorId as any)._id
      ? (patient.doctorId as any)._id.toString()
      : patient.doctorId.toString();

    // Direct match: The logged-in doctor owns the patient
    if (patientDoctorId === doctorId) {
      return patient;
    }

    // Team Access Check: If the logged-in user is an Owner, they can access patients of their team members
    const requestingUser = await User.findById(userId);

    // If user is a primary owner (no ownerId), check if the patient's doctor belongs to them
    if (requestingUser && !requestingUser.ownerId) {
      // Find the doctor record of the patient's assigned doctor
      const patientAssignedDoctor = await Doctor.findById(patientDoctorId);

      if (patientAssignedDoctor) {
        // Find the user account of that doctor
        const patientAssignedDoctorUser = await User.findById(patientAssignedDoctor.userId);

        // If that doctor's ownerId matches the requesting user's ID, allow access
        if (patientAssignedDoctorUser && patientAssignedDoctorUser.ownerId?.toString() === userId) {
          return patient;
        }
      }
    }

    throw new Error('This is not your patient');
  }

  async createPatient(patientData: Partial<IPatient>): Promise<IPatient> {
    const patient = new Patient(patientData);
    await patient.save();
    return (await Patient.findById(patient._id)
      .populate('userId')
      .populate('doctorId')
      .exec()) as IPatient;
  }

  /**
   * Create patient with user
   */
  async createPatientWithUser(data: CreatePatientWithUserData) {
    const { firstName, lastName, email, phone, gender, dateOfBirth, address, medicalHistory, nationalId, insurance, doctorId } = data;

    const normalizedInsurance = insurance?.provider && insurance?.policyNumber
      ? {
        provider: insurance.provider,
        policyNumber: insurance.policyNumber,
        expirationDate: insurance.expirationDate,
      }
      : undefined;

    // Validate required fields
    if (!firstName || !lastName || !email || !doctorId) {
      throw new Error('First name, last name, email, and doctor ID are required');
    }

    // Check if user exists
    const normalizedEmail = email.toLowerCase();
    let user = await User.findOne({ email: normalizedEmail });
    let createdNewUser = false;

    if (!user) {
      // Create new user
      const password = crypto.randomBytes(8).toString('hex');
      user = await User.create({
        firstName,
        lastName,
        email: normalizedEmail,
        password,
        userType: 'patient',
        phone,
        isActive: true
      });
      createdNewUser = true;
    } else {
      // Check if patient record already exists for this user
      const existingPatient = await Patient.findOne({ userId: user._id });
      if (existingPatient) {
        throw new Error('Patient with this email already exists');
      }
    }

    // Create Patient Record
    const patientData = {
      userId: user._id,
      doctorId: new Types.ObjectId(doctorId),
      gender: gender as any,
      dateOfBirth,
      address,
      medicalHistory,
      nationalId,
      insurance: normalizedInsurance,
    };

    const patient = await this.createPatient(patientData);

    // Send patient-created email if new user
    if (createdNewUser) {
      try {
        await EmailService.sendPatientAddedEmail(email, firstName, user.language || 'en');
      } catch (emailError: any) {
        console.error('Failed to send patient-created email:', emailError);
      }
    }

    return {
      patient,
      createdNewUser,
      message: createdNewUser ? 'Patient created and patient email sent.' : 'Patient record created for existing user.'
    };
  }

  async updatePatient(id: string, patientData: Partial<IPatient>): Promise<IPatient | null> {
    return await Patient.findByIdAndUpdate(id, patientData, { new: true })
      .populate('userId')
      .populate('doctorId')
      .exec();
  }

  /**
   * Update patient and user data
   */
  async updatePatientAndUser(id: string, data: any, userId: string): Promise<IPatient | null> {
    // Get doctor ID from user ID
    const doctorId = await this.getDoctorIdFromUserId(userId);
    const { firstName, lastName, email, phone, ...patientData } = data;

    const patient = await Patient.findById(id);
    if (!patient) {
      throw new Error('Patient not found');
    }

    // Verify doctor owns this patient
    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    // Update User data if provided
    if (patient.userId && (firstName || lastName || email || phone)) {
      await User.findByIdAndUpdate(patient.userId, {
        firstName,
        lastName,
        email,
        phone
      });
    }

    // Update Patient data
    return await this.updatePatient(id, patientData);
  }

  async deletePatient(id: string, userId: string): Promise<IPatient | null> {
    // Get doctor ID from user ID
    const doctorId = await this.getDoctorIdFromUserId(userId);

    const patient = await Patient.findById(id);
    if (!patient) {
      throw new Error('Patient not found');
    }

    // Verify doctor owns this patient
    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    return await Patient.findByIdAndDelete(id);
  }

  async getPatientByUserId(userId: string): Promise<IPatient | null> {
    return await Patient.findOne({ userId })
      .populate('userId')
      .populate('doctorId')
      .exec();
  }

  /**
   * Add clinical record to patient
   */
  async addClinicalRecord(data: AddClinicalRecordData, userId: string) {
    // Get doctor ID from user ID
    const doctorId = await this.getDoctorIdFromUserId(userId);
    const { patientId, reasonForConsultation, chiefComplaint, title, ...recordData } = data;

    if (!patientId) {
      throw new Error('Patient ID is required');
    }

    if (!reasonForConsultation && !chiefComplaint && !title) {
      throw new Error('Reason for consultation is required');
    }

    // Verify doctor owns this patient
    const patient = await Patient.findById(patientId);
    if (!patient) {
      throw new Error('Patient not found');
    }

    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    const normalizedRecordData: any = { ...recordData };
    if (Array.isArray(normalizedRecordData.files)) {
      normalizedRecordData.files = sanitizeUrlArray(normalizedRecordData.files);
    }

    const clinicalRecord = await ClinicalRecord.create({
      title,
      reasonForConsultation,
      chiefComplaint,
      ...normalizedRecordData,
      patientId,
      doctorId
    });

    // Build update: push clinical record ref and sync vital signs
    const updateOps: any = {
      $push: { clinicalRecords: clinicalRecord._id }
    };

    // Sync vital signs from clinical record to patient model
    if (recordData.vitalSigns) {
      const vs = recordData.vitalSigns;
      const vitalSignEntry: any = { date: new Date() };
      if (vs.bp) vitalSignEntry.bloodPressure = vs.bp;
      if (vs.hr) vitalSignEntry.heartRate = parseFloat(vs.hr);
      if (vs.temp) vitalSignEntry.temperature = parseFloat(vs.temp);
      if (vs.rr) vitalSignEntry.respiratoryRate = parseFloat(vs.rr);
      if (vs.spo2) vitalSignEntry.oxygenSaturation = parseFloat(vs.spo2);
      updateOps.$push.vitalSigns = { $each: [vitalSignEntry], $position: 0 };
    }

    await Patient.findByIdAndUpdate(patientId, updateOps);

    // Send email notification
    const populatedPatient = await Patient.findById(patientId).populate<{ userId: IUser }>('userId');
    if (populatedPatient && populatedPatient.userId) {
      const user = populatedPatient.userId;
      const date = new Date().toLocaleDateString();
      try {
        await EmailService.sendClinicalRecordEmail(
          user.email,
          user.firstName,
          date,
          title || reasonForConsultation || chiefComplaint || 'General Consultation',
          user.language || 'en'
        );
      } catch (emailError: any) {
        console.error('Failed to send clinical record email:', emailError);
      }
    }

    return clinicalRecord;
  }

  /**
   * Update clinical record
   */
  async updateClinicalRecord(patientId: string, recordId: string, data: any, userId: string) {
    // Get doctor ID from user ID
    const doctorId = await this.getDoctorIdFromUserId(userId);

    // Verify doctor owns this patient
    const patient = await Patient.findById(patientId);
    if (!patient) {
      throw new Error('Patient not found');
    }

    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    // Find and update the clinical record
    const clinicalRecord = await ClinicalRecord.findById(recordId);
    if (!clinicalRecord) {
      throw new Error('Clinical record not found');
    }

    // Verify the record belongs to this patient
    if (clinicalRecord.patientId.toString() !== patientId) {
      throw new Error('Clinical record does not belong to this patient');
    }

    // Update the record
    const normalizedData: any = { ...data };
    if (Array.isArray(normalizedData.files)) {
      normalizedData.files = sanitizeUrlArray(normalizedData.files);
    }

    const updatedRecord = await ClinicalRecord.findByIdAndUpdate(
      recordId,
      { $set: normalizedData },
      { new: true, runValidators: true }
    );

    // Sync vital signs from updated clinical record to patient model
    if (normalizedData.vitalSigns) {
      const vs = normalizedData.vitalSigns;
      const vitalSignEntry: any = { date: new Date() };
      if (vs.bp) vitalSignEntry.bloodPressure = vs.bp;
      if (vs.hr) vitalSignEntry.heartRate = parseFloat(vs.hr);
      if (vs.temp) vitalSignEntry.temperature = parseFloat(vs.temp);
      if (vs.rr) vitalSignEntry.respiratoryRate = parseFloat(vs.rr);
      if (vs.spo2) vitalSignEntry.oxygenSaturation = parseFloat(vs.spo2);
      await Patient.findByIdAndUpdate(patientId, {
        $push: { vitalSigns: { $each: [vitalSignEntry], $position: 0 } }
      });
    }

    return updatedRecord;
  }

  async updateDentalRecord(patientId: string, recordId: string, data: any, userId: string) {
    const doctorId = await this.getDoctorIdFromUserId(userId);

    const patient = await Patient.findById(patientId);
    if (!patient) {
      throw new Error('Patient not found');
    }

    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    const dentalRecord = await DentalRecord.findById(recordId);
    if (!dentalRecord) {
      throw new Error('Dental record not found');
    }

    if (dentalRecord.patientId.toString() !== patientId) {
      throw new Error('Dental record does not belong to this patient');
    }

    const updatedRecord = await DentalRecord.findByIdAndUpdate(
      recordId,
      { $set: data },
      { new: true, runValidators: true }
    );

    return updatedRecord;
  }

  /**
   * Add dental record to patient
   */
  async addDentalRecord(data: AddDentalRecordData, userId: string) {
    // Get doctor ID from user ID
    const doctorId = await this.getDoctorIdFromUserId(userId);
    const { patientId, treatmentPlan, ...recordData } = data;

    if (!patientId) {
      throw new Error('Patient ID is required');
    }

    // Verify doctor owns this patient
    const patient = await Patient.findById(patientId);
    if (!patient) {
      throw new Error('Patient not found');
    }

    if (patient.doctorId.toString() !== doctorId) {
      throw new Error('This is not your patient');
    }

    const dentalRecord = await DentalRecord.create({
      treatmentPlan,
      ...recordData,
      patientId,
      doctorId
    });

    await Patient.findByIdAndUpdate(patientId, {
      $push: { dentalRecords: dentalRecord._id }
    });

    // Send email notification
    const populatedPatient = await Patient.findById(patientId).populate<{ userId: IUser }>('userId');
    if (populatedPatient && populatedPatient.userId) {
      const user = populatedPatient.userId;
      const date = new Date().toLocaleDateString();
      try {
        await EmailService.sendDentalRecordEmail(
          user.email,
          user.firstName,
          date,
          treatmentPlan || 'General Dental Examination',
          user.language || 'en'
        );
      } catch (emailError: any) {
        console.error('Failed to send dental record email:', emailError);
      }
    }

    return dentalRecord;
  }

  async addTestResult(data: AddTestResultData, userId: string) {
    const { patientId, date, type, name, result, fileUrl, notes } = data;

    if (!patientId) {
      throw new Error('Patient ID is required');
    }

    if (!name || !result || !type) {
      throw new Error('Exam name, type, and result are required');
    }

    await this.assertDoctorOwnsPatient(patientId, userId);

    if (isBlobUrl(fileUrl)) {
      throw new Error('Blob file URLs are not supported. Upload the file and save a permanent URL.');
    }

    const examItem = {
      date: date ? new Date(date) : new Date(),
      type,
      name,
      result,
      fileUrl,
      notes
    };

    await Patient.findByIdAndUpdate(patientId, {
      $push: { testResults: examItem }
    });

    return examItem;
  }

  async addDocument(data: AddDocumentData, userId: string) {
    const { patientId, date, name, type, url, category } = data;

    if (!patientId) {
      throw new Error('Patient ID is required');
    }

    if (!name || !type || !url) {
      throw new Error('Document name, type, and file are required');
    }

    if (isBlobUrl(url)) {
      throw new Error('Blob file URLs are not supported. Upload the file and save a permanent URL.');
    }

    await this.assertDoctorOwnsPatient(patientId, userId);

    const documentItem = {
      date: date ? new Date(date) : new Date(),
      name,
      type,
      url,
      category
    };

    await Patient.findByIdAndUpdate(patientId, {
      $push: { documents: documentItem }
    });

    return documentItem;
  }

  async updateTestResult(patientId: string, resultId: string, data: Partial<AddTestResultData>, userId: string) {
    await this.assertDoctorOwnsPatient(patientId, userId);

    const patient = await Patient.findById(patientId);
    if (!patient) {
      throw new Error('Patient not found');
    }

    const idx = (patient.testResults || []).findIndex((item: any) => item?._id?.toString() === resultId);
    if (idx === -1) {
      throw new Error('Exam not found');
    }

    const current = patient.testResults[idx] as any;

    if (data.fileUrl !== undefined && isBlobUrl(data.fileUrl)) {
      throw new Error('Blob file URLs are not supported. Upload the file and save a permanent URL.');
    }

    if (data.date !== undefined) current.date = data.date ? new Date(data.date) : current.date;
    if (data.type !== undefined) current.type = data.type;
    if (data.name !== undefined) current.name = data.name;
    if (data.result !== undefined) current.result = data.result;
    if (data.fileUrl !== undefined) current.fileUrl = data.fileUrl;
    if (data.notes !== undefined) current.notes = data.notes;

    await patient.save();
    return current;
  }

  async deleteTestResult(patientId: string, resultId: string, userId: string) {
    await this.assertDoctorOwnsPatient(patientId, userId);

    const updated = await Patient.findByIdAndUpdate(
      patientId,
      { $pull: { testResults: { _id: this.toObjectId(resultId) } } },
      { new: true }
    );

    if (!updated) throw new Error('Patient not found');
    return { success: true };
  }

  async updateDocument(patientId: string, documentId: string, data: Partial<AddDocumentData>, userId: string) {
    await this.assertDoctorOwnsPatient(patientId, userId);

    const patient = await Patient.findById(patientId);
    if (!patient) {
      throw new Error('Patient not found');
    }

    const idx = (patient.documents || []).findIndex((item: any) => item?._id?.toString() === documentId);
    if (idx === -1) {
      throw new Error('Document not found');
    }

    const current = patient.documents[idx] as any;

    if (data.url !== undefined && isBlobUrl(data.url)) {
      throw new Error('Blob file URLs are not supported. Upload the file and save a permanent URL.');
    }

    if (data.date !== undefined) current.date = data.date ? new Date(data.date) : current.date;
    if (data.name !== undefined) current.name = data.name;
    if (data.type !== undefined) current.type = data.type;
    if (data.url !== undefined) current.url = data.url;
    if (data.category !== undefined) current.category = data.category;

    await patient.save();
    return current;
  }

  async deleteDocument(patientId: string, documentId: string, userId: string) {
    await this.assertDoctorOwnsPatient(patientId, userId);

    const updated = await Patient.findByIdAndUpdate(
      patientId,
      { $pull: { documents: { _id: this.toObjectId(documentId) } } },
      { new: true }
    );

    if (!updated) throw new Error('Patient not found');
    return { success: true };
  }
}

export default new PatientService();
