import { Injectable } from '@nestjs/common';
import { BusinessException } from '@common/exceptions/business.exception';
import {
  DevItemStatus,
  DevItemType,
  DevItemSeverity,
} from '@prisma/client';
import { ItemsRepository } from '../repositories/items.repository';
import { DEVTRACKER_ERROR_CODES } from '../constants/error-codes';

const ITEM_STATUS_TRANSITIONS: Record<DevItemStatus, DevItemStatus[]> = {
  DRAFT: [DevItemStatus.REVIEWED, DevItemStatus.CANCELLED],
  REVIEWED: [DevItemStatus.IN_DEVELOPMENT, DevItemStatus.CANCELLED],
  IN_DEVELOPMENT: [DevItemStatus.IN_TESTING, DevItemStatus.CANCELLED],
  IN_TESTING: [DevItemStatus.USER_TESTING, DevItemStatus.IN_DEVELOPMENT, DevItemStatus.CANCELLED],
  USER_TESTING: [DevItemStatus.DONE, DevItemStatus.IN_DEVELOPMENT, DevItemStatus.CANCELLED],
  DONE: [DevItemStatus.ARCHIVED],
  CANCELLED: [],
  ARCHIVED: [],
};

export interface CurrentUserInfo {
  userId: string;
  roles?: string[];
}

@Injectable()
export class ItemsService {
  constructor(private readonly repository: ItemsRepository) {}

  private isAdmin(roles: string[] = []) {
    return roles.some((role) => role.toUpperCase() === 'ADMINISTRATOR');
  }

  private getTypeAbbr(itemType: DevItemType) {
    switch (itemType) {
      case DevItemType.MODULE:
        return 'MOD';
      case DevItemType.FEATURE:
        return 'FT';
      case DevItemType.BUG:
        return 'BUG';
      default:
        return 'UNK';
    }
  }

  private ensureModuleName(value?: string) {
    if (!value) return undefined;
    const trimmed = value.trim();
    if (!trimmed) return undefined;
    if (/[^A-Za-z0-9 _-]/.test(trimmed)) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.message,
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.code,
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.httpStatus,
      );
    }
    return trimmed;
  }

  private getModuleAbbr(value?: string) {
    const normalized = this.ensureModuleName(value);
    if (!normalized) return 'GEN';
    const words = normalized.split(/[^A-Za-z0-9]+/).filter(Boolean);
    if (words.length <= 1) {
      return words[0]?.slice(0, 3).toUpperCase() || 'GEN';
    }
    const initials = words.map((word) => word[0]).join('');
    return initials.toUpperCase() || 'GEN';
  }

  async findAll(query: any) {
    const { page = 1, pageSize = 20 } = query;
    const result = await this.repository.findMany({
      page,
      pageSize,
      itemType: query.itemType,
      status: query.status,
      ownerId: query.ownerId,
      parentId: query.parentId,
      priority: query.priority,
      keyword: query.keyword,
    });

    return {
      items: result.items,
      pagination: {
        page,
        pageSize,
        total: result.total,
        totalPages: Math.ceil(result.total / pageSize),
      },
    };
  }

  async findById(id: string) {
    const item = await this.repository.findById(id);
    if (!item) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_NOT_FOUND.message,
        DEVTRACKER_ERROR_CODES.ITEM_NOT_FOUND.code,
        DEVTRACKER_ERROR_CODES.ITEM_NOT_FOUND.httpStatus,
      );
    }
    return item;
  }

  async create(payload: {
    title: string;
    description?: string;
    itemType: DevItemType;
    priority: any;
    severity?: DevItemSeverity;
    moduleKey?: string;
    parentId?: string;
    ownerId: string;
    devEtaAt?: string;
    testEtaAt?: string;
    etaAt?: string;
    content?: Record<string, unknown>;
    labels?: string[];
    reporterId: string;
  }) {
    if (payload.itemType !== DevItemType.BUG && payload.severity) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.message,
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.code,
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.httpStatus,
      );
    }

    if (payload.itemType === DevItemType.MODULE && payload.parentId) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.message,
        DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.code,
        DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.httpStatus,
      );
    }

    let parentModuleKey: string | undefined;

    if (payload.itemType !== DevItemType.MODULE) {
      if (!payload.parentId) {
        throw new BusinessException(
          DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.message,
          DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.code,
          DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.httpStatus,
        );
      }
      const parent = await this.repository.findParentItem(payload.parentId);
      if (!parent || parent.itemType !== DevItemType.MODULE) {
        throw new BusinessException(
          DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.message,
          DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.code,
          DEVTRACKER_ERROR_CODES.ITEM_PARENT_INVALID.httpStatus,
        );
      }
      parentModuleKey = parent.moduleKey ?? undefined;
    }

    const moduleAbbr = this.getModuleAbbr(payload.moduleKey || parentModuleKey);
    const typeAbbr = this.getTypeAbbr(payload.itemType);
    const year = new Date().getFullYear();
    const prefix = `DT-${year}-${moduleAbbr}-${typeAbbr}`;

    return this.repository.createWithSequence({
      title: payload.title,
      description: payload.description,
      itemType: payload.itemType,
      priority: payload.priority,
      severity: payload.severity,
      moduleKey: payload.moduleKey,
      parentId: payload.parentId,
      ownerId: payload.ownerId,
      reporterId: payload.reporterId,
      status: DevItemStatus.DRAFT,
      devEtaAt: payload.devEtaAt ? new Date(payload.devEtaAt) : undefined,
      testEtaAt: payload.testEtaAt ? new Date(payload.testEtaAt) : undefined,
      etaAt: payload.etaAt ? new Date(payload.etaAt) : undefined,
      content: payload.content,
      labels: payload.labels,
    }, prefix);
  }

  async update(id: string, payload: any, currentUser: CurrentUserInfo) {
    const item = await this.findById(id);
    if (!this.isAdmin(currentUser.roles) && item.ownerId !== currentUser.userId) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_PERMISSION_DENIED.message,
        DEVTRACKER_ERROR_CODES.ITEM_PERMISSION_DENIED.code,
        DEVTRACKER_ERROR_CODES.ITEM_PERMISSION_DENIED.httpStatus,
      );
    }

    if (payload.severity && item.itemType !== DevItemType.BUG) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.message,
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.code,
        DEVTRACKER_ERROR_CODES.ITEM_VALIDATION_FAILED.httpStatus,
      );
    }

    return this.repository.update(id, {
      ...(payload.title ? { title: payload.title } : {}),
      ...(payload.description !== undefined ? { description: payload.description } : {}),
      ...(payload.priority ? { priority: payload.priority } : {}),
      ...(payload.severity !== undefined ? { severity: payload.severity } : {}),
      ...(payload.ownerId ? { ownerId: payload.ownerId } : {}),
      ...(payload.startAt ? { startAt: new Date(payload.startAt) } : {}),
      ...(payload.devEtaAt ? { devEtaAt: new Date(payload.devEtaAt) } : {}),
      ...(payload.testEtaAt ? { testEtaAt: new Date(payload.testEtaAt) } : {}),
      ...(payload.etaAt ? { etaAt: new Date(payload.etaAt) } : {}),
      ...(payload.content !== undefined ? { content: payload.content } : {}),
      ...(payload.labels !== undefined ? { labels: payload.labels } : {}),
    });
  }

  async updateStatus(
    id: string,
    status: DevItemStatus,
    currentUser: CurrentUserInfo,
  ) {
    const item = await this.findById(id);

    if (!this.canTransitionStatus(item, status, currentUser)) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_STATUS_INVALID.message,
        DEVTRACKER_ERROR_CODES.ITEM_STATUS_INVALID.code,
        DEVTRACKER_ERROR_CODES.ITEM_STATUS_INVALID.httpStatus,
      );
    }

    const updatePayload: any = { status };

    if (status === DevItemStatus.REVIEWED) {
      updatePayload.reviewerId = currentUser.userId;
      updatePayload.reviewedAt = new Date();
    }

    if (status === DevItemStatus.IN_DEVELOPMENT) {
      const now = new Date();
      if (!item.startAt) {
        updatePayload.startAt = now;
      }
      if (!item.devEtaAt) {
        const eta = new Date(now);
        eta.setDate(eta.getDate() + 7);
        updatePayload.devEtaAt = eta;
      }
    }

    if (status === DevItemStatus.IN_TESTING && !item.devCompletedAt) {
      updatePayload.devCompletedAt = new Date();
    }

    if (status === DevItemStatus.USER_TESTING && item.status === DevItemStatus.IN_TESTING) {
      if (!item.testCompletedAt) {
        updatePayload.testCompletedAt = new Date();
      }
    }

    if (status === DevItemStatus.DONE) {
      updatePayload.completedAt = new Date();
    }

    return this.repository.update(id, updatePayload);
  }

  async remove(id: string, currentUser: CurrentUserInfo) {
    const item = await this.findById(id);
    if (!this.isAdmin(currentUser.roles)) {
      throw new BusinessException(
        DEVTRACKER_ERROR_CODES.ITEM_PERMISSION_DENIED.message,
        DEVTRACKER_ERROR_CODES.ITEM_PERMISSION_DENIED.code,
        DEVTRACKER_ERROR_CODES.ITEM_PERMISSION_DENIED.httpStatus,
      );
    }

    return this.repository.update(item.id, { deletedAt: new Date() });
  }

  private canTransitionStatus(
    item: { status: DevItemStatus; ownerId: string },
    next: DevItemStatus,
    currentUser: CurrentUserInfo,
  ) {
    const current = item.status;
    const isAdmin = this.isAdmin(currentUser.roles);

    if (next === DevItemStatus.CANCELLED) {
      return isAdmin;
    }

    if (next === DevItemStatus.ARCHIVED) {
      return isAdmin && current === DevItemStatus.DONE;
    }

    if (next === DevItemStatus.REVIEWED) {
      return ITEM_STATUS_TRANSITIONS[current]?.includes(next) || false;
    }

    if (
      next === DevItemStatus.IN_DEVELOPMENT ||
      next === DevItemStatus.IN_TESTING ||
      next === DevItemStatus.DONE
    ) {
      if (!isAdmin && item.ownerId !== currentUser.userId) {
        return false;
      }
    }

    return ITEM_STATUS_TRANSITIONS[current]?.includes(next) || false;
  }
}
