import { z } from 'zod'
import {
  type CrmPriorityBucketDefinition,
  type CrmPriorityBucketKey,
  type CrmPriorityRuleConfig
} from '../../organization-model/domains/sales'
import { getDealOwnership } from './deal-ownership'
import type { DealPriority } from './types'

export interface CrmPriorityInput {
  stage_key: string | null
  state_key: string | null
  activity_log: unknown
  updated_at: string
  created_at: string
}

export interface EvaluateCrmDealPriorityOptions {
  config?: CrmPriorityRuleConfig | ResolvedCrmPriorityRuleConfig
  now?: Date | string
}

export const CrmPriorityBucketKeySchema = z.enum(['needs_response', 'follow_up_due', 'waiting', 'stale', 'closed_low'])

export const CrmPriorityBucketOverrideSchema = z
  .object({
    label: z.string().trim().min(1).max(80).optional(),
    color: z.string().trim().min(1).max(40).optional(),
    rank: z.number().int().min(0).max(10000).optional()
  })
  .strict()

const CrmPriorityBucketDefinitionSchema = z
  .object({
    bucketKey: CrmPriorityBucketKeySchema,
    label: z.string(),
    rank: z.number().int(),
    color: z.string()
  })
  .strict()

const CrmPriorityRuleConfigSchema = z
  .object({
    enabled: z.boolean().optional(),
    buckets: z.array(CrmPriorityBucketDefinitionSchema),
    closedStageKeys: z.array(z.string()),
    followUpAfterDaysByStateKey: z.record(z.string(), z.number().int()),
    staleAfterDays: z.number().int()
  })
  .strict()

export const CrmPriorityOverrideSchema = z
  .object({
    enabled: z.boolean().optional(),
    staleAfterDays: z.number().int().min(1).max(365).optional(),
    bucketOrder: z.array(CrmPriorityBucketKeySchema).optional(),
    buckets: z.partialRecord(CrmPriorityBucketKeySchema, CrmPriorityBucketOverrideSchema).optional(),
    followUpAfterDaysByStateKey: z
      .record(z.string().trim().min(1).max(120), z.number().int().min(0).max(365))
      .optional(),
    closedStageKeys: z.array(z.string().trim().min(1).max(120)).optional()
  })
  .strict()

export type CrmPriorityBucketOverride = z.infer<typeof CrmPriorityBucketOverrideSchema>
export type CrmPriorityOverride = z.infer<typeof CrmPriorityOverrideSchema>

/**
 * Resolved CRM priority settings used by evaluators.
 *
 * When enabled is false, priority evaluation is intentionally neutral: all deals
 * receive the configured waiting bucket and no follow-up/stale urgency is computed.
 */
export type ResolvedCrmPriorityRuleConfig = CrmPriorityRuleConfig & {
  enabled: boolean
}

interface ParsedActivity {
  type: string | null
  occurredAt: string
}

const MS_PER_DAY = 24 * 60 * 60 * 1000

const DEFAULT_CRM_PRIORITY_BUCKETS: CrmPriorityBucketDefinition[] = [
  { bucketKey: 'needs_response', label: 'Needs Response', rank: 10, color: 'red' },
  { bucketKey: 'follow_up_due', label: 'Follow-up Due', rank: 20, color: 'orange' },
  { bucketKey: 'waiting', label: 'Waiting', rank: 30, color: 'blue' },
  { bucketKey: 'stale', label: 'Stale', rank: 40, color: 'gray' },
  { bucketKey: 'closed_low', label: 'Closed', rank: 50, color: 'dark' }
]

const GENERIC_CRM_PRIORITY_RULE_CONFIG: CrmPriorityRuleConfig = {
  buckets: DEFAULT_CRM_PRIORITY_BUCKETS,
  closedStageKeys: [],
  followUpAfterDaysByStateKey: {},
  staleAfterDays: 14
}

export function evaluateCrmDealPriority(
  input: CrmPriorityInput,
  options: EvaluateCrmDealPriorityOptions = {}
): DealPriority {
  const config = resolveCrmPriorityRuleConfig(options.config)
  const now = parseDate(options.now ?? new Date()) ?? new Date()
  const latestActivityAt = getLatestActivityAt(input)
  const nextActionAt = getNextActionAt(input.state_key, latestActivityAt, config)

  if (!config.enabled) {
    return buildPriority('waiting', config, {
      reason: 'CRM priority evaluation is disabled.',
      latestActivityAt,
      nextActionAt: null
    })
  }

  if (input.stage_key && config.closedStageKeys.includes(input.stage_key)) {
    return buildPriority('closed_low', config, {
      reason: `Deal is in ${input.stage_key}.`,
      latestActivityAt,
      nextActionAt: null
    })
  }

  const ownership = getDealOwnership(input)

  if (ownership === 'us') {
    return buildPriority('needs_response', config, {
      reason: 'Lead replied last — we owe the next move.',
      latestActivityAt,
      nextActionAt
    })
  }

  // ownership === 'them' or null: check age against staleAfterDays first
  if (latestActivityAt) {
    const latestActivityDate = parseDate(latestActivityAt)
    if (latestActivityDate && now.getTime() - latestActivityDate.getTime() >= config.staleAfterDays * MS_PER_DAY) {
      return buildPriority('stale', config, {
        reason: `No meaningful activity in ${config.staleAfterDays} days.`,
        latestActivityAt,
        nextActionAt: null
      })
    }
  }

  if (ownership === 'them') {
    if (nextActionAt) {
      const nextActionDate = parseDate(nextActionAt)
      if (nextActionDate && nextActionDate <= now) {
        return buildPriority('follow_up_due', config, {
          reason: 'Configured follow-up window has elapsed.',
          latestActivityAt,
          nextActionAt
        })
      }
    }

    return buildPriority('waiting', config, {
      reason: 'Waiting on their reply.',
      latestActivityAt,
      nextActionAt
    })
  }

  // ownership === null (no inbound/outbound events): fall back to follow_up/waiting
  if (nextActionAt) {
    const nextActionDate = parseDate(nextActionAt)
    if (nextActionDate && nextActionDate <= now) {
      return buildPriority('follow_up_due', config, {
        reason: 'Configured follow-up window has elapsed.',
        latestActivityAt,
        nextActionAt
      })
    }

    return buildPriority('waiting', config, {
      reason: 'Waiting until the configured next action time.',
      latestActivityAt,
      nextActionAt
    })
  }

  return buildPriority('waiting', config, {
    reason: 'No immediate response or follow-up is due.',
    latestActivityAt,
    nextActionAt: null
  })
}

export function resolveCrmPriorityRuleConfig(
  input?: unknown,
  base: CrmPriorityRuleConfig = GENERIC_CRM_PRIORITY_RULE_CONFIG
): ResolvedCrmPriorityRuleConfig {
  const candidate = extractPriorityOverride(input)
  const fullConfigResult = CrmPriorityRuleConfigSchema.safeParse(candidate)

  if (fullConfigResult.success) return buildResolvedConfig(fullConfigResult.data)

  const result = CrmPriorityOverrideSchema.safeParse(candidate)

  if (!result.success) return buildResolvedConfig(base)

  return mergeCrmPriorityOverride(result.data, base)
}

function mergeCrmPriorityOverride(
  override: CrmPriorityOverride,
  base: CrmPriorityRuleConfig = GENERIC_CRM_PRIORITY_RULE_CONFIG
): ResolvedCrmPriorityRuleConfig {
  const defaultConfig = buildResolvedConfig(base)
  const bucketOverrides = override.buckets ?? {}
  const orderedBucketKeys = override.bucketOrder ? [...new Set(override.bucketOrder)] : []
  const orderRanks = new Map<CrmPriorityBucketKey, number>()

  orderedBucketKeys.forEach((bucketKey, index) => {
    orderRanks.set(bucketKey, (index + 1) * 10)
  })

  const buckets = defaultConfig.buckets
    .map((bucket) => {
      const bucketOverride = bucketOverrides[bucket.bucketKey]

      return {
        ...bucket,
        rank: bucketOverride?.rank ?? orderRanks.get(bucket.bucketKey) ?? bucket.rank,
        label: bucketOverride?.label ?? bucket.label,
        color: bucketOverride?.color ?? bucket.color
      }
    })
    .sort((a, b) => a.rank - b.rank)

  return {
    enabled: override.enabled ?? defaultConfig.enabled,
    buckets,
    closedStageKeys: override.closedStageKeys ?? defaultConfig.closedStageKeys,
    followUpAfterDaysByStateKey: {
      ...defaultConfig.followUpAfterDaysByStateKey,
      ...(override.followUpAfterDaysByStateKey ?? {})
    },
    staleAfterDays: override.staleAfterDays ?? defaultConfig.staleAfterDays
  }
}

function buildResolvedConfig(config: CrmPriorityRuleConfig & { enabled?: boolean }): ResolvedCrmPriorityRuleConfig {
  return {
    enabled: config.enabled ?? true,
    buckets: config.buckets.map((bucket) => ({ ...bucket })),
    closedStageKeys: [...config.closedStageKeys],
    followUpAfterDaysByStateKey: { ...config.followUpAfterDaysByStateKey },
    staleAfterDays: config.staleAfterDays
  }
}

function extractPriorityOverride(input: unknown): unknown {
  if (!isPlainRecord(input)) return input

  const crm = input.crm
  if (isPlainRecord(crm) && Object.prototype.hasOwnProperty.call(crm, 'priority')) {
    return crm.priority
  }

  return input
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}

function buildPriority(
  bucketKey: CrmPriorityBucketKey,
  config: CrmPriorityRuleConfig,
  values: Pick<DealPriority, 'reason' | 'latestActivityAt' | 'nextActionAt'>
): DealPriority {
  const bucket = findBucket(config.buckets, bucketKey)

  return {
    bucketKey: bucket.bucketKey,
    rank: bucket.rank,
    label: bucket.label,
    color: bucket.color,
    reason: values.reason,
    latestActivityAt: values.latestActivityAt,
    nextActionAt: values.nextActionAt
  }
}

function findBucket(
  buckets: CrmPriorityBucketDefinition[],
  bucketKey: CrmPriorityBucketKey
): CrmPriorityBucketDefinition {
  const bucket = buckets.find((candidate) => candidate.bucketKey === bucketKey)
  if (bucket) return bucket

  return {
    bucketKey,
    label: bucketKey,
    rank: Number.MAX_SAFE_INTEGER,
    color: 'gray'
  }
}

function getLatestActivityAt(input: CrmPriorityInput): string | null {
  const timestamps = getParsedActivities(input.activity_log).map((activity) => activity.occurredAt)
  const fallback = normalizeTimestamp(input.updated_at) ?? normalizeTimestamp(input.created_at)
  if (fallback) timestamps.push(fallback)

  return timestamps.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())[0] ?? null
}

function getParsedActivities(activityLog: unknown): ParsedActivity[] {
  if (!Array.isArray(activityLog)) return []

  const parsed: ParsedActivity[] = []

  for (const entry of activityLog) {
    if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue

    const record = entry as Record<string, unknown>
    const occurredAt =
      normalizeTimestamp(record.timestamp) ??
      normalizeTimestamp(record.occurredAt) ??
      normalizeTimestamp(record.createdAt) ??
      normalizeTimestamp(record.updatedAt) ??
      normalizeTimestamp(record.sentAt)

    if (!occurredAt) continue

    parsed.push({
      type: typeof record.type === 'string' ? record.type : null,
      occurredAt
    })
  }

  return parsed
}

function getNextActionAt(
  stateKey: string | null,
  latestActivityAt: string | null,
  config: CrmPriorityRuleConfig
): string | null {
  if (!stateKey || !latestActivityAt) return null

  const days = config.followUpAfterDaysByStateKey[stateKey]
  if (days === undefined) return null

  const latestActivityDate = parseDate(latestActivityAt)
  if (!latestActivityDate) return null

  return new Date(latestActivityDate.getTime() + days * MS_PER_DAY).toISOString()
}

function normalizeTimestamp(value: unknown): string | null {
  const date = parseDate(value)
  return date ? date.toISOString() : null
}

function parseDate(value: unknown): Date | null {
  if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value
  if (typeof value !== 'string') return null

  const date = new Date(value)
  return Number.isNaN(date.getTime()) ? null : date
}
