package com.xyw.console.asset.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xyw.console.asset.dto.DeviceAssetPageQuery;
import com.xyw.console.asset.dto.DeviceAssetPageResponse;
import com.xyw.console.asset.dto.DeviceAssetResponse;
import com.xyw.console.asset.dto.DeviceAssetSaveRequest;
import com.xyw.console.asset.dto.DevicePersonLookupResponse;
import com.xyw.console.asset.entity.AssetDeviceEntity;
import com.xyw.console.asset.entity.CompanyPersonEntity;
import com.xyw.console.asset.entity.DouyinAccountEntity;
import com.xyw.console.asset.entity.PhoneAssetEntity;
import com.xyw.console.asset.entity.WechatAccountEntity;
import com.xyw.console.asset.entity.WecomAccountEntity;
import com.xyw.console.asset.exception.DeviceAssetNotFoundException;
import com.xyw.console.asset.exception.DeviceAssetValidationException;
import com.xyw.console.asset.mapper.AssetDeviceMapper;
import com.xyw.console.asset.mapper.CompanyPersonMapper;
import com.xyw.console.asset.mapper.DouyinAccountMapper;
import com.xyw.console.asset.mapper.PhoneAssetMapper;
import com.xyw.console.asset.mapper.WechatAccountMapper;
import com.xyw.console.asset.mapper.WecomAccountMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

/** File purpose: device asset CRUD, user-name resolution, image references, and reference-safe deletion. */
@Service
public class DeviceAssetService {
  private static final Set<String> USAGE_STATUSES = Set.of("\u4f7f\u7528\u4e2d", "\u95f2\u7f6e", "\u7ef4\u4fee\u4e2d", "\u505c\u7528");
  private static final Set<String> RELATION_STATUSES = Set.of("\u5df2\u5173\u8054", "\u672a\u5173\u8054", "\u5f85\u786e\u8ba4");
  private final AssetDeviceMapper deviceMapper;
  private final CompanyPersonMapper personMapper;
  private final PhoneAssetMapper phoneMapper;
  private final WecomAccountMapper wecomMapper;
  private final WechatAccountMapper wechatMapper;
  private final DouyinAccountMapper douyinMapper;
  private final DeviceAssetFileStorageService fileStorage;

  /** Plain purpose: wire controller-facing persistence and storage collaborators. Related files: DeviceAssetController.java and Mapper classes. Flow: Controller -> Service -> Mapper/file storage. */
  public DeviceAssetService(AssetDeviceMapper deviceMapper, CompanyPersonMapper personMapper, PhoneAssetMapper phoneMapper,
      WecomAccountMapper wecomMapper, WechatAccountMapper wechatMapper, DouyinAccountMapper douyinMapper, DeviceAssetFileStorageService fileStorage) {
    this.deviceMapper=deviceMapper; this.personMapper=personMapper; this.phoneMapper=phoneMapper; this.wecomMapper=wecomMapper; this.wechatMapper=wechatMapper; this.douyinMapper=douyinMapper; this.fileStorage=fileStorage;
  }

  /** Plain purpose: return one active device page with display names. Related files: DeviceAssetPageQuery.java, DeviceAssetController.java. Flow: GET filters -> Mapper page -> response rows -> Vue table. */
  public DeviceAssetPageResponse page(DeviceAssetPageQuery query) {
    Page<AssetDeviceEntity> result=deviceMapper.selectPage(new Page<>(query.resolvedPage(),query.resolvedSize()),activeQuery(query));
    Map<Long,String> names=personNames(collectIds(result.getRecords(),AssetDeviceEntity::getUserPersonId));
    return new DeviceAssetPageResponse(result.getRecords().stream().map(item->toResponse(item,names)).toList(),result.getTotal(),query.resolvedPage(),query.resolvedSize());
  }

  /** Plain purpose: save a new device and its optional images, removing newly stored files if database creation fails. Related files: DeviceAssetSaveRequest.java, DeviceAssetFileStorageService.java. Flow: multipart POST -> store -> insert -> response or cleanup. */
  public DeviceAssetResponse create(DeviceAssetSaveRequest request) {
    validateSaveRequest(request,null); String image1=null,image2=null;
    try {
      image1=fileStorage.store(request.getImageAttachment1()); image2=fileStorage.store(request.getImageAttachment2());
      AssetDeviceEntity entity=new AssetDeviceEntity(); applyEditableFields(entity,request,image1,image2); LocalDateTime now=LocalDateTime.now(); entity.setCreateTime(now); entity.setUpdateTime(now); entity.setDeleteTime(0L);
      if(deviceMapper.insert(entity)!=1) throw new IllegalStateException("\u8bbe\u5907\u8d44\u4ea7\u65b0\u589e\u5931\u8d25");
      return toResponse(entity,personNames(singleId(entity.getUserPersonId())));
    } catch (RuntimeException exception) { fileStorage.cleanupNewFile(image1); fileStorage.cleanupNewFile(image2); throw exception; }
  }

  /** Plain purpose: update an active device while retaining old images unless a new image or remove flag is supplied. Related files: DeviceAssetSaveRequest.java, DeviceAssetFileStorageService.java. Flow: multipart PUT -> replace references -> update -> response. */
  public DeviceAssetResponse update(Long id, DeviceAssetSaveRequest request) {
    AssetDeviceEntity entity=requireActiveDevice(id); validateSaveRequest(request,id); String originalImage1=entity.getImageAttachment1(),originalImage2=entity.getImageAttachment2(),image1=null,image2=null;
    try {
      image1=fileStorage.replace(entity.getImageAttachment1(),request.getImageAttachment1(),Boolean.TRUE.equals(request.getRemoveImageAttachment1()));
      image2=fileStorage.replace(entity.getImageAttachment2(),request.getImageAttachment2(),Boolean.TRUE.equals(request.getRemoveImageAttachment2()));
      applyEditableFields(entity,request,image1,image2); entity.setUpdateTime(LocalDateTime.now());
      if(deviceMapper.updateById(entity)!=1) throw new DeviceAssetNotFoundException("\u8bbe\u5907\u8d44\u4ea7\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664");
      return toResponse(entity,personNames(singleId(entity.getUserPersonId())));
    } catch (RuntimeException exception) {
      if(image1!=null&&!image1.equals(originalImage1)) fileStorage.cleanupNewFile(image1);
      if(image2!=null&&!image2.equals(originalImage2)) fileStorage.cleanupNewFile(image2);
      throw exception;
    }
  }

  /** Plain purpose: soft-delete an unreferenced active device without physically deleting its images. Related files: PhoneAssetEntity.java and account entities. Flow: DELETE -> reference counts -> update deleteTime -> hidden from page. */
  public void softDelete(Long id) {
    AssetDeviceEntity entity=requireActiveDevice(id); checkActiveReferences(id); entity.setDeleteTime(System.currentTimeMillis()); entity.setUpdateTime(LocalDateTime.now());
    if(deviceMapper.updateById(entity)!=1) throw new DeviceAssetNotFoundException("\u8bbe\u5907\u8d44\u4ea7\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664");
  }

  /** Plain purpose: return up to twenty active company persons for remote select. Related files: DeviceAssetController.java, CompanyPersonEntity.java. Flow: select input -> service lookup -> mapper -> dropdown options. */
  public List<DevicePersonLookupResponse> searchCompanyPersons(String keyword) {
    return personMapper.selectList(new LambdaQueryWrapper<CompanyPersonEntity>().eq(CompanyPersonEntity::getDeleteTime,0L).like(hasText(keyword),CompanyPersonEntity::getPersonName,keyword).orderByDesc(CompanyPersonEntity::getId))
        .stream().limit(20).map(item->new DevicePersonLookupResponse(item.getId(),item.getPersonName())).toList();
  }

  /** Plain purpose: resolve a safe image identifier to a controlled resource. Related files: DeviceAssetController.java, DeviceAssetFileStorageService.java. Flow: image URL -> service -> storage -> response body. */
  public Resource findImage(String identifier) { return fileStorage.resolve(identifier); }

  /** Plain purpose: combine active-only and optional page filters. Related files: DeviceAssetPageQuery.java, AssetDeviceEntity.java. Flow: query DTO -> LambdaQueryWrapper -> SQL WHERE. */
  private LambdaQueryWrapper<AssetDeviceEntity> activeQuery(DeviceAssetPageQuery query) {
    return new LambdaQueryWrapper<AssetDeviceEntity>().eq(AssetDeviceEntity::getDeleteTime,0L).like(hasText(query.deviceName()),AssetDeviceEntity::getDeviceName,query.deviceName()).eq(query.userPersonId()!=null,AssetDeviceEntity::getUserPersonId,query.userPersonId()).eq(hasText(query.userUsageStatus()),AssetDeviceEntity::getUserUsageStatus,query.userUsageStatus()).eq(hasText(query.assetRelationStatus()),AssetDeviceEntity::getAssetRelationStatus,query.assetRelationStatus()).orderByDesc(AssetDeviceEntity::getId);
  }

  /** Plain purpose: require one non-deleted device for edit and delete. Related files: AssetDeviceMapper.java, DeviceAssetExceptionHandler.java. Flow: service query -> missing item -> HTTP 404. */
  private AssetDeviceEntity requireActiveDevice(Long id) {
    AssetDeviceEntity entity=deviceMapper.selectOne(new LambdaQueryWrapper<AssetDeviceEntity>().eq(AssetDeviceEntity::getId,id).eq(AssetDeviceEntity::getDeleteTime,0L));
    if(entity==null) throw new DeviceAssetNotFoundException("\u8bbe\u5907\u8d44\u4ea7\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664"); return entity;
  }

  /** Plain purpose: validate names, dropdown values, optional person, and active-name uniqueness before writes. Related files: DeviceAssetSaveRequest.java, CompanyPersonMapper.java. Flow: POST/PUT -> validation -> write or HTTP 400. */
  private void validateSaveRequest(DeviceAssetSaveRequest request,Long currentId) {
    if(request.getDeviceName()==null||request.getDeviceName().isBlank()) throw new DeviceAssetValidationException("\u8bbe\u5907\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a");
    validateStatus(request.getUserUsageStatus(),USAGE_STATUSES,"\u4f7f\u7528\u72b6\u6001"); validateStatus(request.getAssetRelationStatus(),RELATION_STATUSES,"\u8d44\u4ea7\u5173\u8054\u72b6\u6001"); validateUserPerson(request.getUserPersonId());
    long sameName=deviceMapper.selectCount(new LambdaQueryWrapper<AssetDeviceEntity>().eq(AssetDeviceEntity::getDeleteTime,0L).eq(AssetDeviceEntity::getDeviceName,request.getDeviceName().trim()).ne(currentId!=null,AssetDeviceEntity::getId,currentId));
    if(sameName>0) throw new DeviceAssetValidationException("\u8bbe\u5907\u540d\u79f0\u5df2\u5b58\u5728");
  }

  /** Plain purpose: ensure an optional selected person is still active. Related files: CompanyPersonEntity.java, DeviceAssetSaveRequest.java. Flow: form person ID -> mapper count -> allow save or HTTP 400. */
  private void validateUserPerson(Long userPersonId) {
    if(userPersonId==null) return;
    Long count=personMapper.selectCount(new LambdaQueryWrapper<CompanyPersonEntity>().eq(CompanyPersonEntity::getId,userPersonId).eq(CompanyPersonEntity::getDeleteTime,0L));
    if(count==0) throw new DeviceAssetValidationException("\u4f7f\u7528\u4eba\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664");
  }

  /** Plain purpose: reject forged dropdown values. Related files: DeviceAssetView.js, DeviceAssetSaveRequest.java. Flow: dropdown value -> validation -> safe database write. */
  private void validateStatus(String value,Set<String> allowed,String label) { if(value==null||!allowed.contains(value)) throw new DeviceAssetValidationException(label+"\u53d6\u503c\u65e0\u6548"); }

  /** Plain purpose: prevent deletion while any active phone or account references the device. Related files: PhoneAssetEntity.java and account entities. Flow: DELETE -> four reference counts -> error or soft delete. */
  private void checkActiveReferences(Long deviceId) {
    List<String> types=new ArrayList<>();
    if(phoneMapper.selectCount(new LambdaQueryWrapper<PhoneAssetEntity>().eq(PhoneAssetEntity::getDeviceId,deviceId).eq(PhoneAssetEntity::getDeleteTime,0L))>0) types.add("\u624b\u673a\u53f7\u8d44\u4ea7");
    if(wecomMapper.selectCount(new LambdaQueryWrapper<WecomAccountEntity>().eq(WecomAccountEntity::getDeviceId,deviceId).eq(WecomAccountEntity::getDeleteTime,0L))>0) types.add("\u4f01\u4e1a\u5fae\u4fe1\u8d44\u4ea7");
    if(wechatMapper.selectCount(new LambdaQueryWrapper<WechatAccountEntity>().eq(WechatAccountEntity::getDeviceId,deviceId).eq(WechatAccountEntity::getDeleteTime,0L))>0) types.add("\u5fae\u4fe1\u8d44\u4ea7");
    if(douyinMapper.selectCount(new LambdaQueryWrapper<DouyinAccountEntity>().eq(DouyinAccountEntity::getDeviceId,deviceId).eq(DouyinAccountEntity::getDeleteTime,0L))>0) types.add("\u6296\u97f3\u8d44\u4ea7");
    if(!types.isEmpty()) throw new DeviceAssetValidationException("\u8bbe\u5907\u4ecd\u88ab"+String.join("\u3001",types)+"\u5f15\u7528\uff0c\u4e0d\u80fd\u5220\u9664");
  }

  /** Plain purpose: collect non-null person IDs for one batch query. Related files: DeviceAssetResponse.java, CompanyPersonMapper.java. Flow: page records -> IDs -> person query. */
  private <T> Set<Long> collectIds(List<T> items,Function<T,Long> getter) { Set<Long> ids=new HashSet<>(); items.forEach(item->{Long id=getter.apply(item);if(id!=null)ids.add(id);}); return ids; }
  /** Plain purpose: adapt one optional person ID into the batch-name lookup shape. Related files: DeviceAssetService.java, CompanyPersonMapper.java. Flow: created/updated row -> single ID -> personNames. */
  private Set<Long> singleId(Long id) { return id == null ? Set.of() : Set.of(id); }
  /** Plain purpose: resolve person IDs in one query to avoid row-by-row lookups. Related files: CompanyPersonEntity.java, DeviceAssetResponse.java. Flow: IDs -> mapper IN query -> name map -> response. */
  private Map<Long,String> personNames(Set<Long> ids) { if(ids.isEmpty()) return Map.of(); Map<Long,String> result=new HashMap<>(); personMapper.selectList(new LambdaQueryWrapper<CompanyPersonEntity>().in(CompanyPersonEntity::getId,ids).eq(CompanyPersonEntity::getDeleteTime,0L)).forEach(item->result.put(item.getId(),item.getPersonName())); return result; }
  /** Plain purpose: expose safe URLs rather than internal image identifiers or paths. Related files: DeviceAssetResponse.java, DeviceAssetController.java. Flow: entity -> URL conversion -> API JSON -> image tag. */
  private DeviceAssetResponse toResponse(AssetDeviceEntity entity,Map<Long,String> names) { return new DeviceAssetResponse(entity.getId(),entity.getDeviceName(),imageUrl(entity.getImageAttachment1()),imageUrl(entity.getImageAttachment2()),entity.getUserPersonId(),entity.getUserPersonId()==null?null:names.get(entity.getUserPersonId()),entity.getUserUsageStatus(),entity.getAssetRelationStatus(),entity.getCreateTime(),entity.getUpdateTime()); }
  /** Plain purpose: translate a stored opaque image identifier into a controlled URL. Related files: DeviceAssetController.java, DeviceAssetFileStorageService.java. Flow: identifier -> imageUrl -> browser GET. */
  private String imageUrl(String identifier) { return identifier==null||identifier.isBlank()?null:"/api/device-assets/files/"+identifier; }
  /** Plain purpose: copy editable request fields into a database entity. Related files: DeviceAssetSaveRequest.java, AssetDeviceEntity.java. Flow: DTO -> entity -> mapper insert/update. */
  private void applyEditableFields(AssetDeviceEntity entity,DeviceAssetSaveRequest request,String image1,String image2) { entity.setDeviceName(request.getDeviceName().trim()); entity.setUserPersonId(request.getUserPersonId()); entity.setUserUsageStatus(request.getUserUsageStatus()); entity.setAssetRelationStatus(request.getAssetRelationStatus()); entity.setImageAttachment1(image1); entity.setImageAttachment2(image2); }
  /** Plain purpose: decide whether a text filter has meaningful input. Related files: DeviceAssetPageQuery.java, DeviceAssetService.java. Flow: HTTP query -> filter presence -> SQL predicate. */
  private boolean hasText(String value) { return value!=null&&!value.isBlank(); }
}
