/**
 * TimesheetService - Time Tracking Service
 *
 * Provides time entry management with approval workflow.
 * Supports multi-tenant architecture with ServiceContext pattern.
 *
 * @module Projects
 */

import { DataSource, Repository } from 'typeorm';
import { TimesheetEntity, TimesheetStatus } from '../entities/timesheet.entity';
import { ProjectEntity } from '../entities/project.entity';
import { ServiceContext, PaginatedResult } from './project.service';

export interface CreateTimesheetDto {
  companyId: string;
  projectId: string;
  taskId?: string;
  date: string;
  hours: number;
  description?: string;
  billable?: boolean;
}

export interface UpdateTimesheetDto {
  taskId?: string | null;
  date?: string;
  hours?: number;
  description?: string | null;
  billable?: boolean;
}

export interface TimesheetFilters {
  companyId?: string;
  projectId?: string;
  taskId?: string;
  userId?: string;
  status?: TimesheetStatus;
  dateFrom?: string;
  dateTo?: string;
  billable?: boolean;
  invoiced?: boolean;
}

export interface TimesheetSummary {
  totalHours: number;
  billableHours: number;
  nonBillableHours: number;
  approvedHours: number;
  pendingHours: number;
  rejectedHours: number;
}

export class TimesheetService {
  private repository: Repository<TimesheetEntity>;
  private projectRepository: Repository<ProjectEntity>;

  constructor(dataSource: DataSource) {
    this.repository = dataSource.getRepository(TimesheetEntity);
    this.projectRepository = dataSource.getRepository(ProjectEntity);
  }

  /**
   * Find timesheet by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<TimesheetEntity | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
      },
    });
  }

  /**
   * Find all timesheets with pagination and filters
   */
  async findAll(
    ctx: ServiceContext,
    filters: TimesheetFilters = {},
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<TimesheetEntity>> {
    const skip = (page - 1) * limit;

    const qb = this.repository
      .createQueryBuilder('t')
      .where('t.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.companyId) {
      qb.andWhere('t.company_id = :companyId', { companyId: filters.companyId });
    }
    if (filters.projectId) {
      qb.andWhere('t.project_id = :projectId', { projectId: filters.projectId });
    }
    if (filters.taskId) {
      qb.andWhere('t.task_id = :taskId', { taskId: filters.taskId });
    }
    if (filters.userId) {
      qb.andWhere('t.user_id = :userId', { userId: filters.userId });
    }
    if (filters.status) {
      qb.andWhere('t.status = :status', { status: filters.status });
    }
    if (filters.dateFrom) {
      qb.andWhere('t.date >= :dateFrom', { dateFrom: filters.dateFrom });
    }
    if (filters.dateTo) {
      qb.andWhere('t.date <= :dateTo', { dateTo: filters.dateTo });
    }
    if (filters.billable !== undefined) {
      qb.andWhere('t.billable = :billable', { billable: filters.billable });
    }
    if (filters.invoiced !== undefined) {
      qb.andWhere('t.invoiced = :invoiced', { invoiced: filters.invoiced });
    }

    qb.orderBy('t.date', 'DESC').addOrderBy('t.created_at', 'DESC').skip(skip).take(limit);

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Create a new timesheet entry
   */
  async create(ctx: ServiceContext, dto: CreateTimesheetDto): Promise<TimesheetEntity> {
    // Validate hours
    if (dto.hours <= 0 || dto.hours > 24) {
      throw new Error('Hours must be between 0 and 24');
    }

    // Validate project exists and allows timesheets
    const project = await this.projectRepository.findOne({
      where: { id: dto.projectId, tenantId: ctx.tenantId },
    });

    if (!project) {
      throw new Error('Project not found');
    }

    if (!project.allowTimesheets) {
      throw new Error('This project does not allow timesheets');
    }

    const entity = this.repository.create({
      tenantId: ctx.tenantId,
      companyId: dto.companyId,
      projectId: dto.projectId,
      taskId: dto.taskId || null,
      userId: ctx.userId!,
      date: new Date(dto.date),
      hours: dto.hours,
      description: dto.description || null,
      billable: dto.billable ?? true,
      invoiced: false,
      status: TimesheetStatus.DRAFT,
      createdBy: ctx.userId || null,
    });

    return this.repository.save(entity);
  }

  /**
   * Update a timesheet entry
   */
  async update(
    ctx: ServiceContext,
    id: string,
    dto: UpdateTimesheetDto
  ): Promise<TimesheetEntity | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    // Only draft timesheets can be edited
    if (entity.status !== TimesheetStatus.DRAFT) {
      throw new Error('Only draft timesheets can be edited');
    }

    // Only the owner can edit
    if (entity.userId !== ctx.userId) {
      throw new Error('You can only edit your own timesheets');
    }

    if (dto.taskId !== undefined) entity.taskId = dto.taskId;
    if (dto.date !== undefined) entity.date = new Date(dto.date);
    if (dto.hours !== undefined) {
      if (dto.hours <= 0 || dto.hours > 24) {
        throw new Error('Hours must be between 0 and 24');
      }
      entity.hours = dto.hours;
    }
    if (dto.description !== undefined) entity.description = dto.description;
    if (dto.billable !== undefined) entity.billable = dto.billable;

    entity.updatedBy = ctx.userId || null;

    return this.repository.save(entity);
  }

  /**
   * Delete a timesheet entry
   */
  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return false;
    }

    // Only draft timesheets can be deleted
    if (entity.status !== TimesheetStatus.DRAFT) {
      throw new Error('Only draft timesheets can be deleted');
    }

    // Only the owner can delete
    if (entity.userId !== ctx.userId) {
      throw new Error('You can only delete your own timesheets');
    }

    await this.repository.delete(id);
    return true;
  }

  /**
   * Submit timesheet for approval
   */
  async submit(ctx: ServiceContext, id: string): Promise<TimesheetEntity | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    if (entity.status !== TimesheetStatus.DRAFT) {
      throw new Error('Only draft timesheets can be submitted');
    }

    if (entity.userId !== ctx.userId) {
      throw new Error('You can only submit your own timesheets');
    }

    entity.status = TimesheetStatus.SUBMITTED;
    entity.updatedBy = ctx.userId || null;

    return this.repository.save(entity);
  }

  /**
   * Approve a timesheet
   */
  async approve(ctx: ServiceContext, id: string): Promise<TimesheetEntity | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    if (entity.status !== TimesheetStatus.SUBMITTED) {
      throw new Error('Only submitted timesheets can be approved');
    }

    entity.status = TimesheetStatus.APPROVED;
    entity.approvedBy = ctx.userId || null;
    entity.approvedAt = new Date();
    entity.updatedBy = ctx.userId || null;

    return this.repository.save(entity);
  }

  /**
   * Reject a timesheet
   */
  async reject(ctx: ServiceContext, id: string): Promise<TimesheetEntity | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    if (entity.status !== TimesheetStatus.SUBMITTED) {
      throw new Error('Only submitted timesheets can be rejected');
    }

    entity.status = TimesheetStatus.REJECTED;
    entity.updatedBy = ctx.userId || null;

    return this.repository.save(entity);
  }

  /**
   * Get my timesheets (for current user)
   */
  async findMyTimesheets(
    ctx: ServiceContext,
    filters: Omit<TimesheetFilters, 'userId'> = {},
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<TimesheetEntity>> {
    return this.findAll(ctx, { ...filters, userId: ctx.userId }, page, limit);
  }

  /**
   * Get pending approvals
   */
  async findPendingApprovals(
    ctx: ServiceContext,
    filters: Omit<TimesheetFilters, 'status'> = {},
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<TimesheetEntity>> {
    return this.findAll(ctx, { ...filters, status: TimesheetStatus.SUBMITTED }, page, limit);
  }

  /**
   * Get timesheet summary for a project
   */
  async getProjectSummary(ctx: ServiceContext, projectId: string): Promise<TimesheetSummary> {
    const results = await this.repository
      .createQueryBuilder('t')
      .select([
        'SUM(t.hours) as total_hours',
        'SUM(CASE WHEN t.billable = true THEN t.hours ELSE 0 END) as billable_hours',
        'SUM(CASE WHEN t.billable = false THEN t.hours ELSE 0 END) as non_billable_hours',
        'SUM(CASE WHEN t.status = :approved THEN t.hours ELSE 0 END) as approved_hours',
        'SUM(CASE WHEN t.status IN (:...pending) THEN t.hours ELSE 0 END) as pending_hours',
        'SUM(CASE WHEN t.status = :rejected THEN t.hours ELSE 0 END) as rejected_hours',
      ])
      .where('t.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('t.project_id = :projectId', { projectId })
      .setParameters({
        approved: TimesheetStatus.APPROVED,
        pending: [TimesheetStatus.DRAFT, TimesheetStatus.SUBMITTED],
        rejected: TimesheetStatus.REJECTED,
      })
      .getRawOne();

    return {
      totalHours: parseFloat(results?.total_hours || '0'),
      billableHours: parseFloat(results?.billable_hours || '0'),
      nonBillableHours: parseFloat(results?.non_billable_hours || '0'),
      approvedHours: parseFloat(results?.approved_hours || '0'),
      pendingHours: parseFloat(results?.pending_hours || '0'),
      rejectedHours: parseFloat(results?.rejected_hours || '0'),
    };
  }

  /**
   * Get timesheet summary for a user in a date range
   */
  async getUserSummary(
    ctx: ServiceContext,
    userId: string,
    dateFrom: string,
    dateTo: string
  ): Promise<TimesheetSummary> {
    const results = await this.repository
      .createQueryBuilder('t')
      .select([
        'SUM(t.hours) as total_hours',
        'SUM(CASE WHEN t.billable = true THEN t.hours ELSE 0 END) as billable_hours',
        'SUM(CASE WHEN t.billable = false THEN t.hours ELSE 0 END) as non_billable_hours',
        'SUM(CASE WHEN t.status = :approved THEN t.hours ELSE 0 END) as approved_hours',
        'SUM(CASE WHEN t.status IN (:...pending) THEN t.hours ELSE 0 END) as pending_hours',
        'SUM(CASE WHEN t.status = :rejected THEN t.hours ELSE 0 END) as rejected_hours',
      ])
      .where('t.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('t.user_id = :userId', { userId })
      .andWhere('t.date >= :dateFrom', { dateFrom })
      .andWhere('t.date <= :dateTo', { dateTo })
      .setParameters({
        approved: TimesheetStatus.APPROVED,
        pending: [TimesheetStatus.DRAFT, TimesheetStatus.SUBMITTED],
        rejected: TimesheetStatus.REJECTED,
      })
      .getRawOne();

    return {
      totalHours: parseFloat(results?.total_hours || '0'),
      billableHours: parseFloat(results?.billable_hours || '0'),
      nonBillableHours: parseFloat(results?.non_billable_hours || '0'),
      approvedHours: parseFloat(results?.approved_hours || '0'),
      pendingHours: parseFloat(results?.pending_hours || '0'),
      rejectedHours: parseFloat(results?.rejected_hours || '0'),
    };
  }

  /**
   * Mark timesheets as invoiced
   */
  async markAsInvoiced(
    ctx: ServiceContext,
    ids: string[],
    invoiceId: string
  ): Promise<number> {
    const result = await this.repository
      .createQueryBuilder()
      .update(TimesheetEntity)
      .set({ invoiced: true, invoiceId, updatedBy: ctx.userId || null })
      .where('id IN (:...ids)', { ids })
      .andWhere('tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('status = :status', { status: TimesheetStatus.APPROVED })
      .andWhere('invoiced = false')
      .execute();

    return result.affected || 0;
  }

  /**
   * Find billable but not invoiced timesheets
   */
  async findBillableNotInvoiced(
    ctx: ServiceContext,
    projectId?: string,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<TimesheetEntity>> {
    const filters: TimesheetFilters = {
      billable: true,
      invoiced: false,
      status: TimesheetStatus.APPROVED,
    };

    if (projectId) {
      filters.projectId = projectId;
    }

    return this.findAll(ctx, filters, page, limit);
  }
}
