import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { AssetEntity, AssetType } from './entities/asset.entity';
import _, { result } from 'lodash';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
import { createReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express';
import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';

const fileInfo = promisify(stat);

@Injectable()
export class AssetService {
  constructor(
    @InjectRepository(AssetEntity)
    private assetRepository: Repository<AssetEntity>,
  ) {}

  public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
    const asset = new AssetEntity();
    asset.deviceAssetId = assetInfo.deviceAssetId;
    asset.userId = authUser.id;
    asset.deviceId = assetInfo.deviceId;
    asset.type = assetInfo.assetType || AssetType.OTHER;
    asset.originalPath = path;
    asset.createdAt = assetInfo.createdAt;
    asset.modifiedAt = assetInfo.modifiedAt;
    asset.isFavorite = assetInfo.isFavorite;
    asset.mimeType = mimeType;
    asset.duration = assetInfo.duration;

    try {
      const res = await this.assetRepository.save(asset);

      return res;
    } catch (e) {
      Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
    }
  }

  public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
    const rows = await this.assetRepository.find({
      where: {
        userId: authUser.id,
        deviceId: deviceId,
      },
      select: ['deviceAssetId'],
    });

    const res = [];
    rows.forEach((v) => res.push(v.deviceAssetId));
    return res;
  }

  public async getAllAssetsNoPagination(authUser: AuthUserDto) {
    try {
      const assets = await this.assetRepository
        .createQueryBuilder('a')
        .where('a."userId" = :userId', { userId: authUser.id })
        .orderBy('a."createdAt"::date', 'DESC')
        .getMany();

      return assets;
    } catch (e) {
      Logger.error(e, 'getAllAssets');
    }
  }

  public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
    try {
      const assets = await this.assetRepository
        .createQueryBuilder('a')
        .where('a."userId" = :userId', { userId: authUser.id })
        .andWhere('a."createdAt" < :lastQueryCreatedAt', {
          lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
        })
        .orderBy('a."createdAt"::date', 'DESC')
        .take(5000)
        .getMany();

      if (assets.length > 0) {
        const data = _.groupBy(assets, (a) => new Date(a.createdAt).toISOString().slice(0, 10));
        const formattedData = [];
        Object.keys(data).forEach((v) => formattedData.push({ date: v, assets: data[v] }));

        const response = new GetAllAssetReponseDto();
        response.count = assets.length;
        response.data = formattedData;
        response.nextPageKey = assets[assets.length - 1].createdAt;

        return response;
      } else {
        const response = new GetAllAssetReponseDto();
        response.count = 0;
        response.data = [];
        response.nextPageKey = 'null';

        return response;
      }
    } catch (e) {
      Logger.error(e, 'getAllAssets');
    }
  }

  public async findOne(authUser: AuthUserDto, deviceId: string, assetId: string): Promise<AssetEntity> {
    const rows = await this.assetRepository.query(
      'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."userId" = $2 AND a."deviceId" = $3',
      [assetId, authUser.id, deviceId],
    );

    if (rows.lengh == 0) {
      throw new BadRequestException('Not Found');
    }

    return rows[0] as AssetEntity;
  }

  public async getNewAssets(authUser: AuthUserDto, latestDate: string) {
    return await this.assetRepository.find({
      where: {
        userId: authUser.id,
        createdAt: MoreThan(latestDate),
      },
      order: {
        createdAt: 'ASC', // ASC order to add existed asset the latest group first before creating a new date group.
      },
    });
  }

  public async getAssetById(authUser: AuthUserDto, assetId: string) {
    return await this.assetRepository.findOne({
      where: {
        userId: authUser.id,
        id: assetId,
      },
      relations: ['exifInfo'],
    });
  }

  public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
    let file = null;
    const asset = await this.findOne(authUser, query.did, query.aid);

    // Handle Sending Images
    if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
      res.set({
        'Content-Type': asset.mimeType,
      });

      if (query.isThumb === 'false' || !query.isThumb) {
        file = createReadStream(asset.originalPath);
      } else {
        file = createReadStream(asset.resizePath);
      }

      file.on('error', (error) => {
        Logger.log(`Cannot create read stream ${error}`);
        return new BadRequestException('Cannot Create Read Stream');
      });
      return new StreamableFile(file);
    } else if (asset.type == AssetType.VIDEO) {
      // Handle Handling Video
      const { size } = await fileInfo(asset.originalPath);
      const range = headers.range;

      if (range) {
        /** Extracting Start and End value from Range Header */
        let [start, end] = range.replace(/bytes=/, '').split('-');
        start = parseInt(start, 10);
        end = end ? parseInt(end, 10) : size - 1;

        if (!isNaN(start) && isNaN(end)) {
          start = start;
          end = size - 1;
        }
        if (isNaN(start) && !isNaN(end)) {
          start = size - end;
          end = size - 1;
        }

        // Handle unavailable range request
        if (start >= size || end >= size) {
          console.error('Bad Request');
          // Return the 416 Range Not Satisfiable.
          res.status(416).set({
            'Content-Range': `bytes */${size}`,
          });

          throw new BadRequestException('Bad Request Range');
        }

        /** Sending Partial Content With HTTP Code 206 */

        res.status(206).set({
          'Content-Range': `bytes ${start}-${end}/${size}`,
          'Accept-Ranges': 'bytes',
          'Content-Length': end - start + 1,
          'Content-Type': asset.mimeType,
        });

        const videoStream = createReadStream(asset.originalPath, { start: start, end: end });

        return new StreamableFile(videoStream);
      } else {
        res.set({
          'Content-Type': asset.mimeType,
        });

        return new StreamableFile(createReadStream(asset.originalPath));
      }
    }
  }

  public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) {
    let result = [];

    const target = assetIds.ids;
    for (let assetId of target) {
      const res = await this.assetRepository.delete({
        id: assetId,
        userId: authUser.id,
      });

      if (res.affected) {
        result.push({
          id: assetId,
          status: 'success',
        });
      } else {
        result.push({
          id: assetId,
          status: 'failed',
        });
      }
    }

    return result;
  }

  async getAssetSearchTerm(authUser: AuthUserDto): Promise<String[]> {
    const possibleSearchTerm = new Set<String>();
    const rows = await this.assetRepository.query(
      `
      select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
      from assets a
      left join exif e on a.id = e."assetId"
      left join smart_info si on a.id = si."assetId"
      where a."userId" = $1;
      `,
      [authUser.id],
    );

    rows.forEach((row) => {
      // tags
      row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase()));

      // asset's tyoe
      possibleSearchTerm.add(row['type']?.toLowerCase());

      // image orientation
      possibleSearchTerm.add(row['orientation']?.toLowerCase());

      // Lens model
      possibleSearchTerm.add(row['lensModel']?.toLowerCase());

      // Make and model
      possibleSearchTerm.add(row['make']?.toLowerCase());
      possibleSearchTerm.add(row['model']?.toLowerCase());

      // Location
      possibleSearchTerm.add(row['city']?.toLowerCase());
      possibleSearchTerm.add(row['state']?.toLowerCase());
      possibleSearchTerm.add(row['country']?.toLowerCase());
    });

    return Array.from(possibleSearchTerm).filter((x) => x != null);
  }

  async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto) {
    const query = `
    SELECT a.*
    FROM assets a
             LEFT JOIN smart_info si ON a.id = si."assetId"
             LEFT JOIN exif e ON a.id = e."assetId"

    WHERE a."userId" = $1
       AND 
       (
         TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR 
         e.exif_text_searchable_column @@ PLAINTO_TSQUERY('english', $2)
        );
    `;

    const rows = await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);

    return rows;
  }
}
