// ---------------------------------------------------------------------------
// CRM Next-Action evaluator — mirrors the crm-priority.ts resolver/evaluator
// pattern. Maps (state_key, ownership) → suggested action key string.
//
// Per-org overrides live at organizations.config.crm.next_actions and are
// merged at evaluator read time via resolveCrmNextActionRuleConfig.
// ---------------------------------------------------------------------------

import { z } from 'zod'
import { type CrmNextActionRuleConfig, type CrmNextActionMapping } from '../../organization-model/domains/sales'
import { getDealOwnership } from './deal-ownership'
import type { DealOwnership } from './deal-ownership'

export interface CrmNextActionInput {
  stage_key: string | null
  state_key: string | null
  activity_log: unknown
}

export interface EvaluateCrmDealNextActionOptions {
  config?: CrmNextActionRuleConfig | unknown
  now?: Date | string
  /** Current age in days — computed from activity_log if not supplied. */
  ageDays?: number
}

// ---------------------------------------------------------------------------
// Zod schemas for the rule config (used by the resolver)
// ---------------------------------------------------------------------------

const CrmNextActionMappingSchema = z
  .object({
    stateKey: z.string().trim().min(1).max(120),
    ownership: z.enum(['us', 'them', '*']),
    actionKey: z.string().trim().min(1).max(120),
    requiresStale: z.boolean().optional()
  })
  .strict()

const CrmNextActionRuleConfigSchema = z
  .object({
    mappings: z.array(CrmNextActionMappingSchema),
    ownershipUsFallback: z.string().trim().min(1).max(120).nullable().optional()
  })
  .strict()

// Partial override shape accepted from organizations.config.crm.next_actions
export const CrmNextActionOverrideSchema = z
  .object({
    mappings: z.array(CrmNextActionMappingSchema).optional(),
    ownershipUsFallback: z.string().trim().min(1).max(120).nullable().optional()
  })
  .strict()

export type CrmNextActionOverride = z.infer<typeof CrmNextActionOverrideSchema>

/**
 * Resolved next-action config. Always has all required fields populated.
 * `mappings` is the merged list: org overrides prepended before defaults so
 * org-specific rules win on first-match evaluation.
 */
export type ResolvedCrmNextActionRuleConfig = CrmNextActionRuleConfig

const MS_PER_DAY = 24 * 60 * 60 * 1000

const GENERIC_CRM_NEXT_ACTION_RULE_CONFIG: CrmNextActionRuleConfig = {
  mappings: [],
  ownershipUsFallback: null
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Evaluates the suggested next action for a deal.
 *
 * Returns the action key string (matching a key in the caller's action catalog), or
 * null for closed deals or when no mapping matches.
 *
 * Closed deals (ownership === null due to closed stage_key) return null
 * immediately — ownership derivation short-circuits on closed_won/closed_lost.
 */
export function evaluateCrmDealNextAction(
  input: CrmNextActionInput,
  options: EvaluateCrmDealNextActionOptions = {}
): string | null {
  if (isClosedDeal(input)) return null

  const config = resolveCrmNextActionRuleConfig(options.config)
  const ownership = getDealOwnership(input)

  // No ownership signal: staleAfterDays fallback is intentionally not applied here.
  if (ownership === null) return null

  const ageDays = options.ageDays ?? computeAgeDays(input, options.now)

  return (
    evaluateMappings(config.mappings, input.state_key, ownership, ageDays, 14) ??
    (ownership === 'us' ? (config.ownershipUsFallback ?? null) : null)
  )
}

/**
 * Resolves the next-action rule config from an org config input.
 *
 * Accepts:
 * - Full CrmNextActionRuleConfig (used directly after validation)
 * - Partial CrmNextActionOverride (merged on top of defaults)
 * - Nested org config: { crm: { next_actions: ... } }
 * - Any other value → falls back to defaults
 */
export function resolveCrmNextActionRuleConfig(
  input?: unknown,
  base: CrmNextActionRuleConfig = GENERIC_CRM_NEXT_ACTION_RULE_CONFIG
): ResolvedCrmNextActionRuleConfig {
  const candidate = extractNextActionOverride(input)
  const fullResult = CrmNextActionRuleConfigSchema.safeParse(candidate)

  if (fullResult.success) return fullResult.data

  const overrideResult = CrmNextActionOverrideSchema.safeParse(candidate)

  if (!overrideResult.success) return { mappings: [...base.mappings], ownershipUsFallback: base.ownershipUsFallback }

  return mergeNextActionOverride(overrideResult.data, base)
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

function evaluateMappings(
  mappings: CrmNextActionMapping[],
  stateKey: string | null,
  ownership: Exclude<DealOwnership, null>,
  ageDays: number,
  staleAfterDays: number
): string | null {
  for (const mapping of mappings) {
    const stateMatches = mapping.stateKey === '*' || mapping.stateKey === stateKey
    const ownershipMatches = mapping.ownership === '*' || mapping.ownership === ownership
    const staleOk = !mapping.requiresStale || ageDays >= staleAfterDays

    if (stateMatches && ownershipMatches && staleOk) {
      return mapping.actionKey
    }
  }

  return null
}

function isClosedDeal(input: CrmNextActionInput): boolean {
  return (
    input.stage_key === 'closed_won' ||
    input.stage_key === 'closed_lost' ||
    input.state_key === 'closed_won' ||
    input.state_key === 'closed_lost'
  )
}

function mergeNextActionOverride(
  override: CrmNextActionOverride,
  base: CrmNextActionRuleConfig = GENERIC_CRM_NEXT_ACTION_RULE_CONFIG
): ResolvedCrmNextActionRuleConfig {
  // Org mappings are prepended so they win on first-match evaluation.
  const mappings = [...(override.mappings ?? []), ...base.mappings]

  return {
    mappings,
    ownershipUsFallback: override.ownershipUsFallback ?? base.ownershipUsFallback
  }
}

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

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

  return input
}

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

function computeAgeDays(input: CrmNextActionInput, now?: Date | string): number {
  const nowMs = parseMs(now ?? new Date())
  if (nowMs === null) return 0

  if (!Array.isArray(input.activity_log) || input.activity_log.length === 0) return 0

  let latestMs: number | null = null

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

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

    if (ms !== null && (latestMs === null || ms > latestMs)) {
      latestMs = ms
    }
  }

  if (latestMs === null) return 0

  return Math.max(0, (nowMs - latestMs) / MS_PER_DAY)
}

function parseMs(value: unknown): number | null {
  if (value instanceof Date) {
    const ms = value.getTime()
    return Number.isNaN(ms) ? null : ms
  }
  if (typeof value !== 'string') return null
  const ms = new Date(value).getTime()
  return Number.isNaN(ms) ? null : ms
}
