import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { basename, extname, join } from 'path';
import {
  fileManagerContentUrl,
  resolveRootImageUrl,
} from '../common/file-manager-image.util';
import {
  exportRows,
  normalizeImportRow,
  parseImportRows,
} from '../common/tabular-file.util';
import { PrismaService } from '../prisma/prisma.service';

type ProductTagType =
  | 'application'
  | 'load-carry-capacity'
  | 'wheel-size'
  | 'fitting-size'
  | 'body-type'
  | 'finish-type'
  | 'bearing-type'
  | 'wheel-type'
  | 'wheel-type-tag'
  | 'header-type'
  | 'mounting-type'
  | 'dust-cover-type'
  | 'bearing-in-frame-type'
  | 'braking-systems'
  | 'frame-color'
  | 'wheel-color';

type ProductTagQuery = {
  page?: string;
  pageSize?: string;
  search?: string;
  status?: string;
  sortBy?: string;
  sortOrder?: string;
  format?: string;
};

type ProductTagWriteDto = {
  name?: string;
  code?: string;
  status?: string;
  image?: string | null;
  orderId?: string | number | null;
};

type ProductTagModelConfig = {
  prismaKey:
    | 'application'
    | 'loadCarryCapacity'
    | 'wheelSize'
    | 'fittingSize'
    | 'bodyType'
    | 'finishType'
    | 'bearingType'
    | 'wheelType'
    | 'wheelTypeTag'
    | 'headerType'
    | 'mountingType'
    | 'dustCoverType'
    | 'bearingInFrameType'
    | 'brakingSystem'
    | 'frameColor'
    | 'wheelColor';
  title: string;
  hasOrderId: boolean;
  defaultSortBy: string;
};

type ProductTagWriteData = {
  name: string;
  code: string;
  status: string;
  image: string | null;
  orderId?: number | null;
};

const productTagConfigs: Record<ProductTagType, ProductTagModelConfig> = {
  application: {
    prismaKey: 'application',
    title: 'Application',
    hasOrderId: false,
    defaultSortBy: 'code',
  },
  'load-carry-capacity': {
    prismaKey: 'loadCarryCapacity',
    title: 'Load Carry Capacity',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'wheel-size': {
    prismaKey: 'wheelSize',
    title: 'Wheel Size',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'fitting-size': {
    prismaKey: 'fittingSize',
    title: 'Fitting Size',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'body-type': {
    prismaKey: 'bodyType',
    title: 'Body Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'finish-type': {
    prismaKey: 'finishType',
    title: 'Finish Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'bearing-type': {
    prismaKey: 'bearingType',
    title: 'Bearing Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'wheel-type': {
    prismaKey: 'wheelType',
    title: 'Wheel Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'wheel-type-tag': {
    prismaKey: 'wheelTypeTag',
    title: 'Wheel Type Tag',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'header-type': {
    prismaKey: 'headerType',
    title: 'Header Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'mounting-type': {
    prismaKey: 'mountingType',
    title: 'Mounting Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'dust-cover-type': {
    prismaKey: 'dustCoverType',
    title: 'Dust Cover Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'bearing-in-frame-type': {
    prismaKey: 'bearingInFrameType',
    title: 'Bearing In Frame Type',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'braking-systems': {
    prismaKey: 'brakingSystem',
    title: 'Braking Systems',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'frame-color': {
    prismaKey: 'frameColor',
    title: 'Frame Color',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
  'wheel-color': {
    prismaKey: 'wheelColor',
    title: 'Wheel Color',
    hasOrderId: true,
    defaultSortBy: 'orderId',
  },
};

const baseSortColumns = ['code', 'name', 'status', 'updatedAt'];
const orderedSortColumns = ['orderId', ...baseSortColumns];

@Injectable()
export class ProductTagsService {
  constructor(private readonly prisma: PrismaService) {}

  async list(tagType: string, query: ProductTagQuery) {
    const config = this.getConfig(tagType);
    const model = this.getModel(config);
    const page = this.toPositiveNumber(query.page, 1);
    const pageSize = Math.min(this.toPositiveNumber(query.pageSize, 10), 100);
    const sortBy = this.resolveSortBy(config, query.sortBy);
    const sortOrder = query.sortOrder === 'desc' ? 'desc' : 'asc';
    const where = this.buildWhere(query);

    const [total, items, statuses] = await Promise.all([
      model.count({ where }),
      model.findMany({
        where,
        orderBy: this.buildOrderBy(config, sortBy, sortOrder),
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
      model.findMany({
        distinct: ['status'],
        orderBy: { status: 'asc' },
        select: { status: true },
      }),
    ]);

    return {
      items: items.map((item: any) => this.withRootImage(item)),
      meta: {
        total,
        page,
        pageSize,
        totalPages: Math.max(1, Math.ceil(total / pageSize)),
      },
      filterOptions: {
        statuses: statuses.map((item: { status: string }) => item.status),
      },
    };
  }

  async create(tagType: string, dto: ProductTagWriteDto) {
    const config = this.getConfig(tagType);
    const model = this.getModel(config);
    const data = this.validateWrite(config, dto);
    await this.ensureCodeAvailable(model, data.code);

    return this.withRootImage(await model.create({ data }));
  }

  async update(tagType: string, id: string, dto: ProductTagWriteDto) {
    const config = this.getConfig(tagType);
    const model = this.getModel(config);
    await this.ensureExists(model, id, config.title);
    const data = this.validateWrite(config, dto);
    await this.ensureCodeAvailable(model, data.code, id);

    return this.withRootImage(await model.update({ where: { id }, data }));
  }

  async delete(tagType: string, id: string) {
    const config = this.getConfig(tagType);
    const model = this.getModel(config);
    await this.ensureExists(model, id, config.title);
    await model.delete({ where: { id } });

    return { deleted: true };
  }

  async importFile(
    tagType: string,
    fileName: string,
    buffer: Buffer,
    mimeType?: string,
  ) {
    const config = this.getConfig(tagType);
    const model = this.getModel(config);
    const rows = parseImportRows(
      fileName,
      `data:${mimeType || 'application/octet-stream'};base64,${buffer.toString('base64')}`,
    ).map(normalizeImportRow);
    const errors: string[] = [];
    let created = 0;
    let updated = 0;
    let skipped = 0;

    for (const [index, row] of rows.entries()) {
      try {
        const data = this.validateWrite(config, {
          name: row.name,
          code: row.code,
          status: row.status,
          image: row.image || row.imagePath,
          orderId: row.orderId ?? row.order ?? row.displayOrder,
        });

        const existing = await model.findUnique({
          where: { code: data.code },
        });

        await model.upsert({
          where: { code: data.code },
          create: data,
          update: data,
        });

        if (existing) {
          updated += 1;
        } else {
          created += 1;
        }
      } catch (error: any) {
        skipped += 1;
        const message = Array.isArray(error?.response?.message)
          ? error.response.message.join(', ')
          : error?.response?.message || error?.message || 'Invalid row.';
        errors.push(`Row ${index + 2}: ${message}`);
      }
    }

    return {
      totalRows: rows.length,
      created,
      updated,
      skipped,
      errors: errors.slice(0, 25),
    };
  }

  async exportFile(tagType: string, query: ProductTagQuery) {
    const config = this.getConfig(tagType);
    const model = this.getModel(config);
    const rows = await model.findMany({
      where: this.buildWhere(query),
      orderBy: this.buildOrderBy(
        config,
        this.resolveSortBy(config, query.sortBy),
        query.sortOrder === 'desc' ? 'desc' : 'asc',
      ),
    });

    return exportRows(
      `product_tags_${slugify(config.title)}`,
      rows.map((row: any) => this.mapExportRow(config, row)),
      query.format || 'xlsx',
    );
  }

  uploadImage(
    tagType: string,
    originalName: string,
    buffer: Buffer,
    mimeType?: string,
  ) {
    this.getConfig(tagType);

    if (!mimeType || !mimeType.startsWith('image/')) {
      throw new BadRequestException('Only image files are supported.');
    }

    const extension = extname(originalName).toLowerCase() || this.mimeToExtension(mimeType);
    if (!extension) {
      throw new BadRequestException('Unsupported image file type.');
    }

    const uploadRoot = join(process.cwd(), 'storage', 'file-manager');
    if (!existsSync(uploadRoot)) {
      mkdirSync(uploadRoot, { recursive: true });
    }

    const fileName = this.nextAvailableFileName(uploadRoot, this.sanitizeUploadName(originalName, extension));
    writeFileSync(join(uploadRoot, fileName), buffer);

    return {
      image: fileManagerContentUrl(fileName),
    };
  }

  private withRootImage(row: any) {
    const image = resolveRootImageUrl(row.code, row.name, row.image);
    const storedImage = clean(row.image);
    const legacyProductTagUpload = storedImage.startsWith('/uploads/product-tags');

    return {
      ...row,
      image: image || (legacyProductTagUpload ? null : storedImage || null),
    };
  }

  private getConfig(tagType: string): ProductTagModelConfig {
    const config = productTagConfigs[tagType as ProductTagType];

    if (!config) {
      throw new BadRequestException('Unsupported product tag type.');
    }

    return config;
  }

  private getModel(config: ProductTagModelConfig): any {
    return (this.prisma as any)[config.prismaKey];
  }

  private buildWhere(query: ProductTagQuery) {
    const and: any[] = [];
    const search = clean(query.search);

    if (search) {
      and.push({
        OR: [
          { code: { contains: search, mode: 'insensitive' } },
          { name: { contains: search, mode: 'insensitive' } },
          { status: { contains: search, mode: 'insensitive' } },
        ],
      } as any);
    }

    if (clean(query.status)) {
      and.push({
        status: normalizeStatus(query.status),
      } as any);
    }

    return and.length ? ({ AND: and } as any) : {};
  }

  private buildOrderBy(
    config: ProductTagModelConfig,
    sortBy: string,
    sortOrder: 'asc' | 'desc',
  ) {
    if (sortBy === 'orderId' && config.hasOrderId) {
      return [{ orderId: sortOrder }, { code: 'asc' }];
    }

    return [{ [sortBy]: sortOrder }, { code: 'asc' }];
  }

  private resolveSortBy(config: ProductTagModelConfig, value?: string) {
    const allowed = config.hasOrderId ? orderedSortColumns : baseSortColumns;
    return allowed.includes(clean(value)) ? clean(value) : config.defaultSortBy;
  }

  private validateWrite(
    config: ProductTagModelConfig,
    dto: ProductTagWriteDto,
  ): ProductTagWriteData {
    const name = clean(dto.name);
    const code = normalizeCode(dto.code || dto.name);
    const status = normalizeStatus(dto.status);
    const image = clean(dto.image) || null;
    const rawOrderId = clean(dto.orderId);
    const orderId = config.hasOrderId ? parseOptionalInteger(dto.orderId) : undefined;
    const errors: string[] = [];

    if (!name) {
      errors.push(`${config.title} name is required.`);
    }

    if (!code) {
      errors.push(`${config.title} code is required.`);
    }

    if (config.hasOrderId && rawOrderId && orderId === undefined) {
      errors.push('Order ID must be a whole number.');
    }

    if (config.hasOrderId && orderId !== undefined && orderId !== null && orderId < 0) {
      errors.push('Order ID must be zero or greater.');
    }

    if (errors.length) {
      throw new BadRequestException(errors);
    }

    const data: ProductTagWriteData = {
      name,
      code,
      status,
      image,
    };

    if (config.hasOrderId) {
      data.orderId = orderId ?? null;
    }

    return data;
  }

  private async ensureExists(model: any, id: string, title: string) {
    const existing = await model.findUnique({ where: { id } });

    if (!existing) {
      throw new NotFoundException(`${title} record not found.`);
    }
  }

  private async ensureCodeAvailable(model: any, code: string, excludeId?: string) {
    const existing = await model.findUnique({ where: { code } });

    if (existing && existing.id !== excludeId) {
      throw new BadRequestException('Code already exists.');
    }
  }

  private mapExportRow(config: ProductTagModelConfig, row: any) {
    return {
      Code: row.code,
      Name: row.name,
      ...(config.hasOrderId ? { 'Order ID': row.orderId ?? '' } : {}),
      Image: row.image || '',
      Status: row.status,
      'Updated At': row.updatedAt,
    };
  }

  private mimeToExtension(mimeType: string) {
    const mimeMap: Record<string, string> = {
      'image/jpeg': '.jpg',
      'image/png': '.png',
      'image/webp': '.webp',
      'image/gif': '.gif',
      'image/svg+xml': '.svg',
    };

    return mimeMap[mimeType] || '';
  }

  private sanitizeUploadName(originalName: string, extension: string) {
    const baseName = basename(originalName || `image${extension}`)
      .replace(/[^\w .-]+/g, '_')
      .trim();
    if (!baseName) return `image${extension}`;
    return extname(baseName) ? baseName : `${baseName}${extension}`;
  }

  private nextAvailableFileName(directoryPath: string, fileName: string) {
    const extension = extname(fileName);
    const baseName = fileName.slice(0, extension ? -extension.length : undefined);
    let candidateName = fileName;
    let counter = 1;

    while (existsSync(join(directoryPath, candidateName))) {
      candidateName = `${baseName} (${counter})${extension}`;
      counter += 1;
    }

    return candidateName;
  }

  private toPositiveNumber(value: string | undefined, fallback: number) {
    const parsed = Number.parseInt(value || '', 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
  }
}

function clean(value?: string | number | null) {
  return String(value ?? '').trim();
}

function normalizeStatus(value?: string | number | null) {
  return clean(value).toLowerCase() === 'inactive' ? 'Inactive' : 'Active';
}

function normalizeCode(value?: string | number | null) {
  return clean(value)
    .toUpperCase()
    .replace(/[^A-Z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

function parseOptionalInteger(value?: string | number | null) {
  if (value === null || value === undefined || value === '') {
    return undefined;
  }

  const parsed = Number.parseInt(String(value), 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function slugify(value: string) {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
}
