import { Repository, FindOptionsWhere, ILike, IsNull, In } from 'typeorm';
import { Branch, UserBranchAssignment, BranchSchedule, BranchPaymentTerminal } from '../entities';
import { CreateBranchDto, UpdateBranchDto, AssignUserToBranchDto, CreateBranchScheduleDto } from '../dto';

export interface BranchSearchParams {
  search?: string;
  branchType?: string;
  isActive?: boolean;
  parentId?: string;
  includeChildren?: boolean;
  limit?: number;
  offset?: number;
}

export class BranchesService {
  constructor(
    private readonly branchRepository: Repository<Branch>,
    private readonly assignmentRepository: Repository<UserBranchAssignment>,
    private readonly scheduleRepository: Repository<BranchSchedule>,
    private readonly terminalRepository: Repository<BranchPaymentTerminal>
  ) {}

  // ============================================
  // BRANCH CRUD
  // ============================================

  async findAll(tenantId: string, params: BranchSearchParams = {}): Promise<{ data: Branch[]; total: number }> {
    const { search, branchType, isActive, parentId, limit = 50, offset = 0 } = params;

    const where: FindOptionsWhere<Branch> = { tenantId };

    if (branchType) where.branchType = branchType as any;
    if (isActive !== undefined) where.isActive = isActive;
    if (parentId) where.parentId = parentId;
    if (parentId === null) where.parentId = IsNull();

    const queryBuilder = this.branchRepository
      .createQueryBuilder('branch')
      .where('branch.tenant_id = :tenantId', { tenantId })
      .leftJoinAndSelect('branch.schedules', 'schedules')
      .leftJoinAndSelect('branch.paymentTerminals', 'terminals');

    if (search) {
      queryBuilder.andWhere('(branch.name ILIKE :search OR branch.code ILIKE :search OR branch.city ILIKE :search)', {
        search: `%${search}%`,
      });
    }

    if (branchType) {
      queryBuilder.andWhere('branch.branch_type = :branchType', { branchType });
    }

    if (isActive !== undefined) {
      queryBuilder.andWhere('branch.is_active = :isActive', { isActive });
    }

    if (parentId) {
      queryBuilder.andWhere('branch.parent_id = :parentId', { parentId });
    } else if (parentId === null) {
      queryBuilder.andWhere('branch.parent_id IS NULL');
    }

    queryBuilder.orderBy('branch.hierarchy_path', 'ASC').addOrderBy('branch.name', 'ASC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder.skip(offset).take(limit).getMany();

    return { data, total };
  }

  async findOne(id: string): Promise<Branch | null> {
    return this.branchRepository.findOne({
      where: { id },
      relations: ['parent', 'children', 'schedules', 'paymentTerminals', 'userAssignments'],
    });
  }

  async findByCode(tenantId: string, code: string): Promise<Branch | null> {
    return this.branchRepository.findOne({
      where: { tenantId, code },
      relations: ['schedules', 'paymentTerminals'],
    });
  }

  async create(tenantId: string, dto: CreateBranchDto, createdBy?: string): Promise<Branch> {
    // Check for duplicate code
    const existing = await this.findByCode(tenantId, dto.code);
    if (existing) {
      throw new Error(`Branch with code '${dto.code}' already exists`);
    }

    // Build hierarchy path
    let hierarchyPath = `/${dto.code}`;
    let hierarchyLevel = 0;

    if (dto.parentId) {
      const parent = await this.findOne(dto.parentId);
      if (!parent) {
        throw new Error('Parent branch not found');
      }
      hierarchyPath = `${parent.hierarchyPath}/${dto.code}`;
      hierarchyLevel = parent.hierarchyLevel + 1;
    }

    const branch = this.branchRepository.create({
      ...dto,
      tenantId,
      hierarchyPath,
      hierarchyLevel,
      createdBy,
    });

    return this.branchRepository.save(branch);
  }

  async update(id: string, dto: UpdateBranchDto, updatedBy?: string): Promise<Branch | null> {
    const branch = await this.findOne(id);
    if (!branch) return null;

    // If changing parent, update hierarchy
    if (dto.parentId !== undefined && dto.parentId !== branch.parentId) {
      if (dto.parentId) {
        const newParent = await this.findOne(dto.parentId);
        if (!newParent) {
          throw new Error('New parent branch not found');
        }

        // Check for circular reference
        if (newParent.hierarchyPath.includes(`/${branch.code}/`) || newParent.id === branch.id) {
          throw new Error('Cannot create circular reference in branch hierarchy');
        }

        branch.hierarchyPath = `${newParent.hierarchyPath}/${branch.code}`;
        branch.hierarchyLevel = newParent.hierarchyLevel + 1;
      } else {
        branch.hierarchyPath = `/${branch.code}`;
        branch.hierarchyLevel = 0;
      }

      // Update children hierarchy paths
      await this.updateChildrenHierarchy(branch);
    }

    Object.assign(branch, dto, { updatedBy });
    return this.branchRepository.save(branch);
  }

  private async updateChildrenHierarchy(parent: Branch): Promise<void> {
    const children = await this.branchRepository.find({
      where: { parentId: parent.id },
    });

    for (const child of children) {
      child.hierarchyPath = `${parent.hierarchyPath}/${child.code}`;
      child.hierarchyLevel = parent.hierarchyLevel + 1;
      await this.branchRepository.save(child);
      await this.updateChildrenHierarchy(child);
    }
  }

  async delete(id: string): Promise<boolean> {
    const branch = await this.findOne(id);
    if (!branch) return false;

    // Check if has children
    const childrenCount = await this.branchRepository.count({ where: { parentId: id } });
    if (childrenCount > 0) {
      throw new Error('Cannot delete branch with children. Delete children first or move them to another parent.');
    }

    await this.branchRepository.softDelete(id);
    return true;
  }

  // ============================================
  // HIERARCHY
  // ============================================

  async getHierarchy(tenantId: string): Promise<Branch[]> {
    const branches = await this.branchRepository.find({
      where: { tenantId, isActive: true },
      order: { hierarchyPath: 'ASC' },
    });

    return this.buildTree(branches);
  }

  private buildTree(branches: Branch[], parentId: string | null = null): Branch[] {
    return branches
      .filter((b) => b.parentId === parentId)
      .map((branch) => ({
        ...branch,
        children: this.buildTree(branches, branch.id),
      }));
  }

  async getChildren(branchId: string, recursive: boolean = false): Promise<Branch[]> {
    if (!recursive) {
      return this.branchRepository.find({
        where: { parentId: branchId, isActive: true },
        order: { name: 'ASC' },
      });
    }

    const parent = await this.findOne(branchId);
    if (!parent) return [];

    return this.branchRepository
      .createQueryBuilder('branch')
      .where('branch.hierarchy_path LIKE :path', { path: `${parent.hierarchyPath}/%` })
      .andWhere('branch.is_active = true')
      .orderBy('branch.hierarchy_path', 'ASC')
      .getMany();
  }

  async getParents(branchId: string): Promise<Branch[]> {
    const branch = await this.findOne(branchId);
    if (!branch || !branch.hierarchyPath) return [];

    const codes = branch.hierarchyPath.split('/').filter((c) => c && c !== branch.code);
    if (codes.length === 0) return [];

    return this.branchRepository.find({
      where: { tenantId: branch.tenantId, code: In(codes) },
      order: { hierarchyLevel: 'ASC' },
    });
  }

  // ============================================
  // USER ASSIGNMENTS
  // ============================================

  async assignUser(tenantId: string, dto: AssignUserToBranchDto, assignedBy?: string): Promise<UserBranchAssignment> {
    // Check if branch exists
    const branch = await this.findOne(dto.branchId);
    if (!branch || branch.tenantId !== tenantId) {
      throw new Error('Branch not found');
    }

    // Check for existing assignment of same type
    const existing = await this.assignmentRepository.findOne({
      where: {
        userId: dto.userId,
        branchId: dto.branchId,
        assignmentType: (dto.assignmentType as any) ?? 'primary',
      },
    });

    if (existing) {
      // Update existing
      Object.assign(existing, {
        branchRole: dto.branchRole ?? existing.branchRole,
        permissions: dto.permissions ?? existing.permissions,
        validUntil: dto.validUntil ? new Date(dto.validUntil) : existing.validUntil,
        isActive: true,
      });
      return this.assignmentRepository.save(existing);
    }

    const assignment = this.assignmentRepository.create({
      userId: dto.userId,
      branchId: dto.branchId,
      tenantId,
      assignmentType: (dto.assignmentType || 'primary') as any,
      branchRole: dto.branchRole as any,
      permissions: dto.permissions || [],
      validUntil: dto.validUntil ? new Date(dto.validUntil) : undefined,
      createdBy: assignedBy,
    });

    return this.assignmentRepository.save(assignment) as Promise<UserBranchAssignment>;
  }

  async unassignUser(userId: string, branchId: string): Promise<boolean> {
    const result = await this.assignmentRepository.update({ userId, branchId }, { isActive: false });
    return (result.affected ?? 0) > 0;
  }

  async getUserBranches(userId: string): Promise<Branch[]> {
    const assignments = await this.assignmentRepository.find({
      where: { userId, isActive: true },
      relations: ['branch'],
    });

    return assignments.map((a) => a.branch).filter((b) => b != null);
  }

  async getBranchUsers(branchId: string): Promise<UserBranchAssignment[]> {
    return this.assignmentRepository.find({
      where: { branchId, isActive: true },
      order: { branchRole: 'ASC' },
    });
  }

  async getPrimaryBranch(userId: string): Promise<Branch | null> {
    const assignment = await this.assignmentRepository.findOne({
      where: { userId, assignmentType: 'primary' as any, isActive: true },
      relations: ['branch'],
    });

    return assignment?.branch ?? null;
  }

  // ============================================
  // GEOFENCING
  // ============================================

  async validateGeofence(branchId: string, latitude: number, longitude: number): Promise<{ valid: boolean; distance: number }> {
    const branch = await this.findOne(branchId);
    if (!branch) {
      throw new Error('Branch not found');
    }

    if (!branch.geofenceEnabled) {
      return { valid: true, distance: 0 };
    }

    if (!branch.latitude || !branch.longitude) {
      return { valid: true, distance: 0 };
    }

    // Calculate distance using Haversine formula
    const R = 6371000; // Earth's radius in meters
    const dLat = this.toRad(latitude - branch.latitude);
    const dLon = this.toRad(longitude - branch.longitude);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this.toRad(branch.latitude)) * Math.cos(this.toRad(latitude)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    const distance = R * c;

    return {
      valid: distance <= branch.geofenceRadius,
      distance: Math.round(distance),
    };
  }

  private toRad(deg: number): number {
    return deg * (Math.PI / 180);
  }

  async findNearbyBranches(tenantId: string, latitude: number, longitude: number, radiusMeters: number = 5000): Promise<Branch[]> {
    // Use PostgreSQL's earthdistance extension if available, otherwise calculate in app
    const branches = await this.branchRepository.find({
      where: { tenantId, isActive: true },
    });

    return branches
      .filter((b) => {
        if (!b.latitude || !b.longitude) return false;
        const result = this.calculateDistance(latitude, longitude, b.latitude, b.longitude);
        return result <= radiusMeters;
      })
      .sort((a, b) => {
        const distA = this.calculateDistance(latitude, longitude, a.latitude!, a.longitude!);
        const distB = this.calculateDistance(latitude, longitude, b.latitude!, b.longitude!);
        return distA - distB;
      });
  }

  private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
    const R = 6371000;
    const dLat = this.toRad(lat2 - lat1);
    const dLon = this.toRad(lon2 - lon1);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }

  // ============================================
  // SCHEDULES
  // ============================================

  async addSchedule(branchId: string, dto: CreateBranchScheduleDto): Promise<BranchSchedule> {
    const schedule = this.scheduleRepository.create({
      ...dto,
      branchId,
      specificDate: dto.specificDate ? new Date(dto.specificDate) : undefined,
    });

    return this.scheduleRepository.save(schedule);
  }

  async getSchedules(branchId: string): Promise<BranchSchedule[]> {
    return this.scheduleRepository.find({
      where: { branchId, isActive: true },
      order: { dayOfWeek: 'ASC', specificDate: 'ASC' },
    });
  }

  async isOpenNow(branchId: string): Promise<boolean> {
    const schedules = await this.getSchedules(branchId);
    const now = new Date();
    const dayOfWeek = now.getDay();
    const currentTime = now.toTimeString().slice(0, 5);

    // Check for specific date schedule first
    const today = now.toISOString().slice(0, 10);
    const specificSchedule = schedules.find((s) => s.specificDate?.toISOString().slice(0, 10) === today);

    if (specificSchedule) {
      return currentTime >= specificSchedule.openTime && currentTime <= specificSchedule.closeTime;
    }

    // Check regular schedule
    const regularSchedule = schedules.find((s) => s.dayOfWeek === dayOfWeek && s.scheduleType === 'regular');

    if (regularSchedule) {
      return currentTime >= regularSchedule.openTime && currentTime <= regularSchedule.closeTime;
    }

    return false;
  }

  // ============================================
  // MAIN BRANCH
  // ============================================

  async getMainBranch(tenantId: string): Promise<Branch | null> {
    return this.branchRepository.findOne({
      where: { tenantId, isMain: true, isActive: true },
      relations: ['schedules', 'paymentTerminals'],
    });
  }

  async setAsMainBranch(branchId: string): Promise<Branch | null> {
    const branch = await this.findOne(branchId);
    if (!branch) return null;

    // Unset current main branch
    await this.branchRepository.update({ tenantId: branch.tenantId, isMain: true }, { isMain: false });

    // Set new main branch
    branch.isMain = true;
    return this.branchRepository.save(branch);
  }
}
