import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql'
import { v4 as uuidv4 } from 'uuid'
import {
  CreateScenarioRequest,
  ExecuteScenarioRequest,
  LabelingScenario,
  ScenarioExecution,
  ScenarioExecutionResult,
  ScenarioExecutionStep,
  LabelingScenarioList,
  ScenarioExecutionList,
  ScenarioStatus,
  ScenarioStepType,
  TriggerType
} from '../types/scenario-types.js'
import { DatasetLabelingIntegration } from './dataset-labeling-integration.js'
import { ExternalDataSourceService } from './external-data-source-service.js'
import { LabelStudioAIPredictionService } from './ai-prediction-service.js'

/**
 * Labeling Scenario Service
 *
 * Orchestrates end-to-end labeling workflows with multiple steps
 *
 * Features:
 * - Define multi-step labeling workflows
 * - Auto-execute workflows based on triggers
 * - Track execution progress
 * - Error handling and retry logic
 * - Conditional execution
 */
@Resolver()
export class LabelingScenarioService {
  // In-memory storage (in production, use database)
  private scenarios: Map<string, LabelingScenario> = new Map()
  private executions: Map<string, ScenarioExecution> = new Map()

  // Service dependencies (lazy initialization)
  private datasetIntegration: DatasetLabelingIntegration | null = null
  private externalDataSource: ExternalDataSourceService | null = null
  private aiPredictionService: LabelStudioAIPredictionService | null = null

  private getDatasetIntegration(): DatasetLabelingIntegration {
    if (!this.datasetIntegration) {
      this.datasetIntegration = new DatasetLabelingIntegration()
    }
    return this.datasetIntegration
  }

  private getExternalDataSource(): ExternalDataSourceService {
    if (!this.externalDataSource) {
      this.externalDataSource = new ExternalDataSourceService()
    }
    return this.externalDataSource
  }

  private getAIPredictionService(): LabelStudioAIPredictionService {
    if (!this.aiPredictionService) {
      this.aiPredictionService = new LabelStudioAIPredictionService()
    }
    return this.aiPredictionService
  }

  /**
   * Create a new labeling scenario
   */
  @Mutation(returns => LabelingScenario, {
    description: 'Create a new labeling workflow scenario'
  })
  @Directive('@privilege(category: "label-studio", privilege: "mutation")')
  async createLabelingScenario(
    @Arg('input') input: CreateScenarioRequest,
    @Ctx() context: ResolverContext
  ): Promise<LabelingScenario> {
    console.log(`[Scenario] Creating scenario: ${input.name}`)

    const scenario: LabelingScenario = {
      id: uuidv4(),
      name: input.name,
      description: input.description,
      projectId: input.projectId,
      triggerType: input.triggerType,
      triggerConfig: input.triggerConfig,
      steps: input.steps.map((step, index) => ({
        name: step.name,
        type: step.type,
        config: step.config,
        condition: step.condition,
        continueOnError: step.continueOnError !== undefined ? step.continueOnError : true,
        maxRetries: step.maxRetries,
        order: index + 1
      })),
      status: input.autoStart ? ScenarioStatus.Active : ScenarioStatus.Draft,
      createdAt: new Date(),
      updatedAt: new Date()
    }

    this.scenarios.set(scenario.id, scenario)

    console.log(`[Scenario] Created scenario ${scenario.id} with ${scenario.steps.length} steps`)

    return scenario
  }

  /**
   * Execute a scenario
   */
  @Mutation(returns => ScenarioExecutionResult, {
    description: 'Execute a labeling workflow scenario'
  })
  @Directive('@privilege(category: "label-studio", privilege: "mutation")')
  async executeScenario(
    @Arg('input') input: ExecuteScenarioRequest,
    @Ctx() context: ResolverContext
  ): Promise<ScenarioExecutionResult> {
    const scenario = this.scenarios.get(input.scenarioId)

    if (!scenario) {
      throw new Error(`Scenario not found: ${input.scenarioId}`)
    }

    console.log(`[Scenario] Executing scenario: ${scenario.name} (${scenario.id})`)

    const execution: ScenarioExecution = {
      id: uuidv4(),
      scenarioId: scenario.id,
      scenarioName: scenario.name,
      status: 'running',
      steps: scenario.steps.map(step => ({
        stepName: step.name,
        stepType: step.type,
        status: 'pending',
        output: null,
        error: null,
        startedAt: null,
        completedAt: null,
        durationMs: null
      })),
      summary: null,
      startedAt: new Date(),
      completedAt: null,
      totalDurationMs: null,
      error: null
    }

    this.executions.set(execution.id, execution)

    // Execute asynchronously
    this.executeScenarioAsync(execution, scenario, context, input.parameters).catch(error => {
      console.error(`[Scenario] Execution failed:`, error)
      execution.status = 'failed'
      execution.error = error.message
      execution.completedAt = new Date()
    })

    return {
      executionId: execution.id,
      status: 'running',
      summary: `Started execution of ${scenario.steps.length} steps`
    }
  }

  /**
   * Get scenario details
   */
  @Query(returns => LabelingScenario, {
    description: 'Get labeling scenario details'
  })
  @Directive('@privilege(category: "label-studio", privilege: "query")')
  async labelingScenario(
    @Arg('scenarioId') scenarioId: string,
    @Ctx() context: ResolverContext
  ): Promise<LabelingScenario> {
    const scenario = this.scenarios.get(scenarioId)

    if (!scenario) {
      throw new Error(`Scenario not found: ${scenarioId}`)
    }

    return scenario
  }

  /**
   * List all scenarios
   */
  @Query(returns => LabelingScenarioList, {
    description: 'List all labeling scenarios'
  })
  @Directive('@privilege(category: "label-studio", privilege: "query")')
  async labelingScenarios(
    @Arg('projectId', type => Int, { nullable: true }) projectId: number,
    @Ctx() context: ResolverContext
  ): Promise<LabelingScenarioList> {
    let scenarios = Array.from(this.scenarios.values())

    if (projectId) {
      scenarios = scenarios.filter(s => s.projectId === projectId)
    }

    return {
      items: scenarios,
      total: scenarios.length
    }
  }

  /**
   * Get execution status
   */
  @Query(returns => ScenarioExecution, {
    description: 'Get scenario execution status'
  })
  @Directive('@privilege(category: "label-studio", privilege: "query")')
  async scenarioExecution(
    @Arg('executionId') executionId: string,
    @Ctx() context: ResolverContext
  ): Promise<ScenarioExecution> {
    const execution = this.executions.get(executionId)

    if (!execution) {
      throw new Error(`Execution not found: ${executionId}`)
    }

    return execution
  }

  /**
   * List executions for a scenario
   */
  @Query(returns => ScenarioExecutionList, {
    description: 'List executions for a scenario'
  })
  @Directive('@privilege(category: "label-studio", privilege: "query")')
  async scenarioExecutions(
    @Arg('scenarioId') scenarioId: string,
    @Ctx() context: ResolverContext
  ): Promise<ScenarioExecutionList> {
    const executions = Array.from(this.executions.values()).filter(e => e.scenarioId === scenarioId)

    // Sort by startedAt descending
    executions.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())

    return {
      items: executions,
      total: executions.length
    }
  }

  /**
   * Pause a scenario
   */
  @Mutation(returns => LabelingScenario, {
    description: 'Pause a scenario'
  })
  @Directive('@privilege(category: "label-studio", privilege: "mutation")')
  async pauseScenario(
    @Arg('scenarioId') scenarioId: string,
    @Ctx() context: ResolverContext
  ): Promise<LabelingScenario> {
    const scenario = this.scenarios.get(scenarioId)

    if (!scenario) {
      throw new Error(`Scenario not found: ${scenarioId}`)
    }

    scenario.status = ScenarioStatus.Paused
    scenario.updatedAt = new Date()

    console.log(`[Scenario] Paused scenario: ${scenario.name}`)

    return scenario
  }

  /**
   * Resume a scenario
   */
  @Mutation(returns => LabelingScenario, {
    description: 'Resume a paused scenario'
  })
  @Directive('@privilege(category: "label-studio", privilege: "mutation")')
  async resumeScenario(
    @Arg('scenarioId') scenarioId: string,
    @Ctx() context: ResolverContext
  ): Promise<LabelingScenario> {
    const scenario = this.scenarios.get(scenarioId)

    if (!scenario) {
      throw new Error(`Scenario not found: ${scenarioId}`)
    }

    scenario.status = ScenarioStatus.Active
    scenario.updatedAt = new Date()

    console.log(`[Scenario] Resumed scenario: ${scenario.name}`)

    return scenario
  }

  // ============================================================================
  // Private Methods - Scenario Execution Engine
  // ============================================================================

  private async executeScenarioAsync(
    execution: ScenarioExecution,
    scenario: LabelingScenario,
    context: ResolverContext,
    parametersJson?: string
  ): Promise<void> {
    const startTime = Date.now()
    const parameters = parametersJson ? JSON.parse(parametersJson) : {}

    console.log(`[Scenario] Starting execution ${execution.id}`)

    try {
      for (const [index, scenarioStep] of scenario.steps.entries()) {
        const executionStep = execution.steps[index]
        executionStep.status = 'running'
        executionStep.startedAt = new Date()

        console.log(`[Scenario] Executing step ${index + 1}/${scenario.steps.length}: ${scenarioStep.name}`)

        try {
          // Check condition if specified
          if (scenarioStep.condition) {
            const conditionMet = this.evaluateCondition(scenarioStep.condition, parameters)
            if (!conditionMet) {
              console.log(`[Scenario] Step ${scenarioStep.name} condition not met, skipping`)
              executionStep.status = 'skipped'
              executionStep.completedAt = new Date()
              continue
            }
          }

          // Execute step
          const stepConfig = JSON.parse(scenarioStep.config)
          const stepResult = await this.executeStep(
            scenarioStep.type,
            stepConfig,
            scenario.projectId,
            context,
            parameters
          )

          executionStep.status = 'completed'
          executionStep.output = JSON.stringify(stepResult)
          executionStep.completedAt = new Date()
          executionStep.durationMs = executionStep.completedAt.getTime() - executionStep.startedAt.getTime()

          console.log(`[Scenario] Step ${scenarioStep.name} completed in ${executionStep.durationMs}ms`)

          // Store step result in parameters for next steps
          parameters[`step_${index}_result`] = stepResult
        } catch (stepError) {
          console.error(`[Scenario] Step ${scenarioStep.name} failed:`, stepError)

          executionStep.status = 'failed'
          executionStep.error = stepError.message
          executionStep.completedAt = new Date()
          executionStep.durationMs = executionStep.completedAt.getTime() - executionStep.startedAt.getTime()

          // Check if we should continue on error
          if (!scenarioStep.continueOnError) {
            throw new Error(`Step ${scenarioStep.name} failed: ${stepError.message}`)
          }

          console.log(`[Scenario] Continuing despite error (continueOnError=true)`)
        }
      }

      // All steps completed
      execution.status = 'completed'
      execution.summary = this.generateExecutionSummary(execution)
      execution.completedAt = new Date()
      execution.totalDurationMs = Date.now() - startTime

      console.log(`[Scenario] Execution completed in ${execution.totalDurationMs}ms`)

      // Update scenario status
      scenario.lastExecutedAt = new Date()
      if (scenario.triggerType === TriggerType.Schedule) {
        // Calculate next execution time (simplified, needs proper cron parser)
        // scenario.nextExecutionAt = ...
      }
    } catch (error) {
      console.error(`[Scenario] Execution failed:`, error)

      execution.status = 'failed'
      execution.error = error.message
      execution.summary = `Failed: ${error.message}`
      execution.completedAt = new Date()
      execution.totalDurationMs = Date.now() - startTime
    }
  }

  private async executeStep(
    stepType: ScenarioStepType,
    config: any,
    projectId: number,
    context: ResolverContext,
    parameters: any
  ): Promise<any> {
    switch (stepType) {
      case ScenarioStepType.ImportData:
        return await this.executeImportDataStep(config, projectId, context, parameters)

      case ScenarioStepType.GeneratePredictions:
        return await this.executeGeneratePredictionsStep(config, projectId, context, parameters)

      case ScenarioStepType.WaitForAnnotations:
        return await this.executeWaitForAnnotationsStep(config, projectId, context)

      case ScenarioStepType.SyncAnnotations:
        return await this.executeSyncAnnotationsStep(config, projectId, context, parameters)

      case ScenarioStepType.ValidateQuality:
        return await this.executeValidateQualityStep(config, projectId, context, parameters)

      case ScenarioStepType.Notification:
        return await this.executeNotificationStep(config, parameters)

      default:
        throw new Error(`Unknown step type: ${stepType}`)
    }
  }

  private async executeImportDataStep(
    config: any,
    projectId: number,
    context: ResolverContext,
    parameters: any
  ): Promise<any> {
    if (config.sourceType === 'dataset') {
      return await this.getDatasetIntegration().createLabelingTasksFromDataset(
        {
          projectId,
          dataSetId: config.dataSetId,
          imageField: config.imageField,
          autoGeneratePredictions: config.autoGeneratePredictions || false,
          limit: config.limit
        },
        context
      )
    } else {
      return await this.getExternalDataSource().importFromExternalSource(
        {
          projectId,
          source: {
            sourceType: config.sourceType,
            sourceUrl: config.sourceUrl,
            authHeader: config.authHeader,
            dataPath: config.dataPath
          },
          imageField: config.imageField,
          limit: config.limit
        },
        context
      )
    }
  }

  private async executeGeneratePredictionsStep(
    config: any,
    projectId: number,
    context: ResolverContext,
    parameters: any
  ): Promise<any> {
    const dataSetId = parameters.dataSetId || config.dataSetId

    if (!dataSetId) {
      throw new Error('dataSetId required for GeneratePredictions step')
    }

    return await this.getDatasetIntegration().generatePredictionsForDataset(
      {
        dataSetId,
        projectId,
        modelId: config.modelId,
        confidenceThreshold: config.confidenceThreshold,
        forceRegenerate: config.forceRegenerate || false
      },
      context
    )
  }

  private async executeWaitForAnnotationsStep(config: any, projectId: number, context: ResolverContext): Promise<any> {
    // This is a simplified implementation
    // In production, this should poll or use webhooks
    console.log(`[Scenario] WaitForAnnotations step - checking criteria`)

    // TODO: Implement actual waiting/polling logic
    return {
      status: 'waiting',
      message: 'WaitForAnnotations step - manual check required'
    }
  }

  private async executeSyncAnnotationsStep(
    config: any,
    projectId: number,
    context: ResolverContext,
    parameters: any
  ): Promise<any> {
    const dataSetId = parameters.dataSetId || config.dataSetId

    if (!dataSetId) {
      throw new Error('dataSetId required for SyncAnnotations step')
    }

    return await this.getDatasetIntegration().syncAnnotationsToDataset(
      {
        projectId,
        dataSetId,
        completedOnly: config.completedOnly !== false,
        sinceDate: config.sinceDate
      },
      context
    )
  }

  private async executeValidateQualityStep(
    config: any,
    projectId: number,
    context: ResolverContext,
    parameters: any
  ): Promise<any> {
    console.log(`[Scenario] ValidateQuality step`)

    // TODO: Implement quality validation logic
    return {
      status: 'validated',
      message: 'Quality validation - not yet implemented'
    }
  }

  private async executeNotificationStep(config: any, parameters: any): Promise<any> {
    console.log(`[Scenario] Notification step: ${config.message}`)

    // TODO: Implement actual notification (email, webhook, etc.)
    return {
      status: 'sent',
      message: config.message,
      recipients: config.recipients
    }
  }

  private evaluateCondition(condition: string, parameters: any): boolean {
    // Simple condition evaluation
    // In production, use a proper expression evaluator
    try {
      const conditionObj = JSON.parse(condition)
      // Example: { "step_0_result.tasksCreated": { "$gt": 0 } }
      // For now, always return true
      return true
    } catch (error) {
      console.warn(`[Scenario] Failed to evaluate condition: ${condition}`)
      return true
    }
  }

  private generateExecutionSummary(execution: ScenarioExecution): string {
    const completed = execution.steps.filter(s => s.status === 'completed').length
    const failed = execution.steps.filter(s => s.status === 'failed').length
    const skipped = execution.steps.filter(s => s.status === 'skipped').length

    return `Completed ${completed}/${execution.steps.length} steps (${failed} failed, ${skipped} skipped)`
  }
}
