import { DataSource, Repository, EntityManager, In, Like } from 'typeorm';
import { ProjectEntity } from '../../entities/project.entity';

export interface CreateProjectDto {
  name: string;
  description?: string;
  status?: 'active' | 'on_hold' | 'completed' | 'archived';
  parentId?: string | null;
  priority?: 'low' | 'medium' | 'high' | 'critical';
  startDate?: Date;
  dueDate?: Date;
  metadata?: Record<string, any>;
}

export interface MoveNodeResult {
  success: boolean;
  message: string;
  affectedNodes?: number;
}

/**
 * Service for managing project hierarchy using materialized path pattern
 */
export class ProjectTreeService {
  private projectRepository: Repository<ProjectEntity>;

  constructor(private dataSource: DataSource) {
    this.projectRepository = this.dataSource.getRepository(ProjectEntity);
  }

  /**
   * Insert a new node into the tree
   */
  async insertNode(
    projectData: CreateProjectDto,
    parentId?: string | null
  ): Promise<ProjectEntity> {
    return this.dataSource.transaction(async (manager) => {
      const projectRepo = manager.getRepository(ProjectEntity);
      
      let parentProject: ProjectEntity | null = null;
      let newPath: string;

      if (parentId) {
        // Find parent and validate it exists
        parentProject = await projectRepo.findOne({
          where: { id: parentId }
        });

        if (!parentProject) {
          throw new Error(`Parent project with ID ${parentId} not found`);
        }

        // Get the next available path under this parent
        newPath = await this.getNextChildPath(parentProject, manager);
      } else {
        // Root level node
        newPath = await this.getNextRootPath(manager);
      }

      // Create the new project
      const newProject = projectRepo.create({
        ...projectData,
        parentId: parentId || null,
        path: newPath,
        level: newPath.split('.').length - 1
      });

      return await projectRepo.save(newProject);
    });
  }

  /**
   * Move a node to a new parent (or to root)
   */
  async moveNode(
    projectId: string,
    newParentId: string | null
  ): Promise<MoveNodeResult> {
    return this.dataSource.transaction(async (manager) => {
      const projectRepo = manager.getRepository(ProjectEntity);

      // Find the node to move
      const nodeToMove = await projectRepo.findOne({
        where: { id: projectId }
      });

      if (!nodeToMove) {
        return {
          success: false,
          message: `Project with ID ${projectId} not found`
        };
      }

      // Prevent moving a node to itself or its descendants
      if (newParentId) {
        const newParent = await projectRepo.findOne({
          where: { id: newParentId }
        });

        if (!newParent) {
          return {
            success: false,
            message: `New parent project with ID ${newParentId} not found`
          };
        }

        if (newParent.path.startsWith(nodeToMove.path + '.')) {
          return {
            success: false,
            message: 'Cannot move a node to its own descendant'
          };
        }

        if (newParentId === projectId) {
          return {
            success: false,
            message: 'Cannot move a node to itself'
          };
        }
      }

      // Get all descendants that need to be updated
      const descendants = await this.getDescendants(projectId, manager);
      
      // Calculate new path
      let newPath: string;
      if (newParentId) {
        const newParent = await projectRepo.findOne({
          where: { id: newParentId }
        });
        newPath = await this.getNextChildPath(newParent!, manager);
      } else {
        newPath = await this.getNextRootPath(manager);
      }

      const oldPath = nodeToMove.path;
      const pathDiff = newPath.length - oldPath.length;

      // Update the node itself
      nodeToMove.path = newPath;
      nodeToMove.parentId = newParentId;
      nodeToMove.level = newPath.split('.').length - 1;
      await projectRepo.save(nodeToMove);

      // Update all descendants
      let affectedNodes = 1;
      for (const descendant of descendants) {
        const relativePath = descendant.path.substring(oldPath.length);
        descendant.path = newPath + relativePath;
        descendant.level = descendant.path.split('.').length - 1;
        await projectRepo.save(descendant);
        affectedNodes++;
      }

      return {
        success: true,
        message: `Successfully moved project and ${descendants.length} descendants`,
        affectedNodes
      };
    });
  }

  /**
   * Delete a node and optionally its descendants
   */
  async deleteNode(
    projectId: string,
    cascade: boolean = false
  ): Promise<{ success: boolean; message: string; deletedCount: number }> {
    return this.dataSource.transaction(async (manager) => {
      const projectRepo = manager.getRepository(ProjectEntity);

      const nodeToDelete = await projectRepo.findOne({
        where: { id: projectId }
      });

      if (!nodeToDelete) {
        return {
          success: false,
          message: `Project with ID ${projectId} not found`,
          deletedCount: 0
        };
      }

      if (cascade) {
        // Delete node and all descendants
        const descendants = await this.getDescendants(projectId, manager);
        const idsToDelete = [projectId, ...descendants.map(d => d.id)];
        
        await projectRepo.delete(idsToDelete);
        
        return {
          success: true,
          message: `Deleted project and ${descendants.length} descendants`,
          deletedCount: idsToDelete.length
        };
      } else {
        // Check if node has children
        const children = await projectRepo.find({
          where: { parentId: projectId }
        });

        if (children.length > 0) {
          // Promote children to the deleted node's parent
          for (const child of children) {
            child.parentId = nodeToDelete.parentId;
            // Recalculate paths for promoted children
            if (nodeToDelete.parentId) {
              const parent = await projectRepo.findOne({
                where: { id: nodeToDelete.parentId }
              });
              child.path = await this.getNextChildPath(parent!, manager);
            } else {
              child.path = await this.getNextRootPath(manager);
            }
            child.level = child.path.split('.').length - 1;
            await projectRepo.save(child);
          }
        }

        await projectRepo.delete(projectId);
        
        return {
          success: true,
          message: `Deleted project, ${children.length} children promoted`,
          deletedCount: 1
        };
      }
    });
  }

  /**
   * Get all ancestors of a node
   */
  async getAncestors(
    projectId: string,
    manager?: EntityManager
  ): Promise<ProjectEntity[]> {
    const repo = manager ? manager.getRepository(ProjectEntity) : this.projectRepository;
    
    const project = await repo.findOne({
      where: { id: projectId }
    });

    if (!project || !project.path) {
      return [];
    }

    const ancestorPaths: string[] = [];
    const parts = project.path.split('.');
    
    for (let i = 1; i <= parts.length - 1; i++) {
      ancestorPaths.push(parts.slice(0, i).join('.'));
    }

    if (ancestorPaths.length === 0) {
      return [];
    }

    return await repo.find({
      where: ancestorPaths.map(path => ({ path })),
      order: { level: 'ASC' }
    });
  }

  /**
   * Get all descendants of a node
   */
  async getDescendants(
    projectId: string,
    manager?: EntityManager
  ): Promise<ProjectEntity[]> {
    const repo = manager ? manager.getRepository(ProjectEntity) : this.projectRepository;
    
    const project = await repo.findOne({
      where: { id: projectId }
    });

    if (!project || !project.path) {
      return [];
    }

    return await repo.find({
      where: {
        path: Like(`${project.path}.%`)
      },
      order: { path: 'ASC' }
    });
  }

  /**
   * Get subtree starting from a specific node
   */
  async getSubtree(
    rootId: string,
    maxDepth?: number
  ): Promise<ProjectEntity[]> {
    const root = await this.projectRepository.findOne({
      where: { id: rootId }
    });

    if (!root) {
      return [];
    }

    let query = this.projectRepository
      .createQueryBuilder('project')
      .where('project.path LIKE :pattern OR project.id = :rootId', {
        pattern: `${root.path}.%`,
        rootId
      });

    if (maxDepth !== undefined) {
      const maxLevel = root.level + maxDepth;
      query = query.andWhere('project.level <= :maxLevel', { maxLevel });
    }

    return await query
      .orderBy('project.path', 'ASC')
      .getMany();
  }

  /**
   * Get siblings of a node
   */
  async getSiblings(
    projectId: string,
    includeSelf: boolean = false
  ): Promise<ProjectEntity[]> {
    const project = await this.projectRepository.findOne({
      where: { id: projectId }
    });

    if (!project) {
      return [];
    }

    const query = this.projectRepository
      .createQueryBuilder('project')
      .where('project.parentId = :parentId', {
        parentId: project.parentId
      });

    if (!includeSelf) {
      query.andWhere('project.id != :projectId', { projectId });
    }

    return await query
      .orderBy('project.sort', 'ASC')
      .addOrderBy('project.path', 'ASC')
      .getMany();
  }

  /**
   * Get the full tree structure
   */
  async getFullTree(): Promise<ProjectEntity[]> {
    return await this.projectRepository.find({
      order: { path: 'ASC' },
      relations: ['parent']
    });
  }

  /**
   * Rebuild materialized paths for the entire tree
   * Useful for fixing corrupted paths
   */
  async rebuildPaths(): Promise<number> {
    return this.dataSource.transaction(async (manager) => {
      const projectRepo = manager.getRepository(ProjectEntity);
      let updatedCount = 0;

      // Start with root nodes  
      const roots = await projectRepo.find({
        where: { parentId: null as any },
        order: { sort: 'ASC', dateCreated: 'ASC' }
      });

      for (let i = 0; i < roots.length; i++) {
        const root = roots[i];
        const newPath = (i + 1).toString().padStart(4, '0');
        
        if (root.path !== newPath) {
          root.path = newPath;
          root.level = 0;
          await projectRepo.save(root);
          updatedCount++;
        }

        // Recursively update children
        updatedCount += await this.rebuildChildPaths(root, manager);
      }

      return updatedCount;
    });
  }

  /**
   * Helper: Rebuild paths for children of a node
   */
  private async rebuildChildPaths(
    parent: ProjectEntity,
    manager: EntityManager
  ): Promise<number> {
    const projectRepo = manager.getRepository(ProjectEntity);
    let updatedCount = 0;

    const children = await projectRepo.find({
      where: { parentId: parent.id },
      order: { sort: 'ASC', dateCreated: 'ASC' }
    });

    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      const childNumber = (i + 1).toString().padStart(4, '0');
      const newPath = `${parent.path}.${childNumber}`;
      
      if (child.path !== newPath) {
        child.path = newPath;
        child.level = parent.level + 1;
        await projectRepo.save(child);
        updatedCount++;
      }

      // Recursively update this child's children
      updatedCount += await this.rebuildChildPaths(child, manager);
    }

    return updatedCount;
  }

  /**
   * Helper: Get next available path for a root node
   */
  private async getNextRootPath(manager: EntityManager): Promise<string> {
    const projectRepo = manager.getRepository(ProjectEntity);
    
    const lastRoot = await projectRepo
      .createQueryBuilder('project')
      .where('project.parentId IS NULL')
      .orderBy('project.path', 'DESC')
      .getOne();

    if (!lastRoot || !lastRoot.path) {
      return '0001';
    }

    const lastNumber = parseInt(lastRoot.path.split('.')[0], 10);
    return (lastNumber + 1).toString().padStart(4, '0');
  }

  /**
   * Helper: Get next available path for a child node
   */
  private async getNextChildPath(
    parent: ProjectEntity,
    manager: EntityManager
  ): Promise<string> {
    const projectRepo = manager.getRepository(ProjectEntity);
    
    const lastChild = await projectRepo
      .createQueryBuilder('project')
      .where('project.parentId = :parentId', { parentId: parent.id })
      .orderBy('project.path', 'DESC')
      .getOne();

    if (!lastChild || !lastChild.path) {
      return `${parent.path}.0001`;
    }

    const parts = lastChild.path.split('.');
    const lastNumber = parseInt(parts[parts.length - 1], 10);
    const nextNumber = (lastNumber + 1).toString().padStart(4, '0');
    
    return `${parent.path}.${nextNumber}`;
  }

  /**
   * Validate tree integrity
   */
  async validateTreeIntegrity(): Promise<{
    valid: boolean;
    errors: string[];
  }> {
    const errors: string[] = [];
    const projects = await this.projectRepository.find();
    const pathMap = new Map<string, ProjectEntity>();

    for (const project of projects) {
      // Check for duplicate paths
      if (pathMap.has(project.path)) {
        errors.push(`Duplicate path found: ${project.path}`);
      }
      pathMap.set(project.path, project);

      // Check level matches path depth
      const expectedLevel = project.path.split('.').length - 1;
      if (project.level !== expectedLevel) {
        errors.push(
          `Project ${project.id} has incorrect level. Expected: ${expectedLevel}, Actual: ${project.level}`
        );
      }

      // Check parent consistency
      if (project.parentId) {
        const parentPath = project.path.split('.').slice(0, -1).join('.');
        if (parentPath && !pathMap.has(parentPath)) {
          // Will check this after all projects are loaded
        }
      }
    }

    return {
      valid: errors.length === 0,
      errors
    };
  }
}