import Joi from 'joi';
import mongoose, { Model } from 'mongoose';

import { MODEL_NAMES } from './_models';

export interface IScoringRule {
  score: number;
  equals?: string | number | boolean;
  lt?: number;
  lte?: number;
  gt?: number;
  gte?: number;
  between?: [number, number];
}

export type TScorecardVersionCriterionTypes = typeof SCORECARD_CRITERION_TYPES[number];

export const SCORECARD_CRITERION_TYPES = [
  'boolean',
  'enum',
  'number',
  'range',
  'rating',
] as const;

export const SCORECARD_CRITERION_TYPE = {
  BOOLEAN: 'boolean',
  ENUM: 'enum',
  NUMBER: 'number',
  RANGE: 'range',
  RATING: 'rating',
} as const satisfies Record<Uppercase<TScorecardVersionCriterionTypes>, TScorecardVersionCriterionTypes>;

export interface IScorecardDefinition {
  sections: {
    key: string;
    label: string;
    order: number;
    maxScore: number;
    criteria: {
      key: string;
      label: string;
      type: TScorecardVersionCriterionTypes;
      options?: { value: string; label: string }[];
      min?: number;
      max?: number;
      step?: number;
      prefix?: string;
      suffix?: string;
      rules: IScoringRule[];
    }[];
  }[];
}

export type TScorecardVersionStatus = typeof SCORECARD_VERSION_STATUSES[number];

export const SCORECARD_VERSION_STATUSES = [
  'draft',
  'active',
  'archived',
] as const;

export const SCORECARD_VERSION_STATUS = {
  DRAFT: 'draft',
  ACTIVE: 'active',
  ARCHIVED: 'archived',
} as const satisfies Record<Uppercase<TScorecardVersionStatus>, TScorecardVersionStatus>;

export const RISK_RATINGS = [
  'PASS',
  'SPECIAL_MENTION',
  'SUBSTANDARD',
  'DOUBTFUL',
] as const;

export type TRiskRating = typeof RISK_RATINGS[number];

export interface IDecisionRule {
  when: {
    key: string;
    equals: string | number | boolean;
  };
  result: TRiskRating;
  priority: number;
}

export interface IScoreBand {
  min: number;
  max?: number;
  result: TRiskRating;
}

export interface IScorecardVersion {
  scorecardId: mongoose.Types.ObjectId;
  version: string;
  status: TScorecardVersionStatus;
  validFrom: Date;
  validTo?: Date;
  definition: IScorecardDefinition;
  decisionRules: IDecisionRule[];
  scoreBands: IScoreBand[];
}

export interface IScorecardVersionDoc extends IScorecardVersion, Document {
  _id: mongoose.Types.ObjectId;
  createdAt: Date;
  updatedAt: Date;
}

export interface IScorecardVersionLean extends IScorecardVersion {
  _id: mongoose.Types.ObjectId;
  createdAt: Date;
  updatedAt: Date;
}

export interface IScorecardVersionPlain extends IScorecardVersion {
  _id: string;
}

export type TScorecardVersionModel = Model<IScorecardVersion>;


const scoringRuleSchema = Joi.object({
  score: Joi.number().required(),

  // mutually exclusive conditions
  equals: Joi.alternatives(Joi.string(), Joi.number(), Joi.boolean()),
  lt: Joi.number(),
  lte: Joi.number(),
  gt: Joi.number(),
  gte: Joi.number(),
  between: Joi.array().items(Joi.number()).length(2),
})
  .xor('equals', 'lt', 'lte', 'gt', 'gte', 'between')
  .required();

const criterionSchema = Joi.object({
  key: Joi.string()
    .regex(/^[a-z][a-zA-Z0-9]*$/)
    .required(),

  label: Joi.string().required(),

  type: Joi.string()
    .valid(...SCORECARD_CRITERION_TYPES)
    .required(),

  options: Joi.when('type', {
    is: 'enum',
    then: Joi.array()
      .items(
        Joi.object({
          value: Joi.string().required(),
          label: Joi.string().required(),
        })
      )
      .min(1)
      .required(),
    otherwise: Joi.forbidden(),
  }),

  min: Joi.when('type', {
    is: Joi.valid('number', 'range', 'rating'),
    then: Joi.number().optional(),
    otherwise: Joi.forbidden(),
  }),

  max: Joi.when('type', {
    is: Joi.valid('number', 'range', 'rating'),
    then: Joi.number().optional(),
    otherwise: Joi.forbidden(),
  }),

  step: Joi.when('type', {
    is: Joi.valid('number', 'range', 'rating'),
    then: Joi.number().optional(),
    otherwise: Joi.forbidden(),
  }),

  prefix: Joi.when('type', {
    is: Joi.valid('number', 'range', 'rating'),
    then: Joi.string().optional(),
    otherwise: Joi.forbidden(),
  }),

  suffix: Joi.when('type', {
    is: Joi.valid('number', 'range', 'rating'),
    then: Joi.string().optional(),
    otherwise: Joi.forbidden(),
  }),

  rules: Joi.array()
    .items(scoringRuleSchema)
    .min(1)
    .required(),
});

const sectionSchema = Joi.object({
  key: Joi.string()
    .regex(/^[a-z][a-zA-Z0-9]*$/)
    .required(),

  label: Joi.string().required(),

  order: Joi.number().integer().min(1).required(),

  maxScore: Joi.number().required(),

  criteria: Joi.array()
    .items(criterionSchema)
    .min(1)
    .required(),
});

export const ScorecardDefinitionJoiSchema = Joi.object({
  sections: Joi.array()
    .items(sectionSchema)
    .min(1)
    .required(),
}).required();

const DecisionRuleJoiSchema = Joi.object({
  when: Joi.object({
    key: Joi.string().required(),
    equals: Joi.alternatives(
      Joi.string(),
      Joi.number(),
      Joi.boolean()
    ).required(),
  }).required(),

  result: Joi.string()
    .valid(...RISK_RATINGS)
    .required(),

  priority: Joi.number().integer().min(1).required(),
});

export const DecisionRulesJoiSchema = Joi.array()
  .items(DecisionRuleJoiSchema)
  .min(0)
  .required();

const ScoreBandJoiSchema = Joi.object({
  min: Joi.number().required(),
  max: Joi.number().optional(),
  result: Joi.string()
    .valid(...RISK_RATINGS)
    .required(),
});

export const ScoreBandsJoiSchema = Joi.array()
  .items(ScoreBandJoiSchema)
  .min(1)
  .required();

const DecisionRuleSchema = new mongoose.Schema<IDecisionRule>(
  {
    when: {
      key: { type: String, required: true },
      equals: {
        type: mongoose.Schema.Types.Mixed,
        required: true,
      },
    },
    result: {
      type: String,
      enum: RISK_RATINGS,
      required: true,
    },
    priority: {
      type: Number,
      required: true,
    },
  },
  { _id: false }
);

const ScoreBandSchema = new mongoose.Schema<IScoreBand>(
  {
    min: { type: Number, required: true },
    max: { type: Number },
    result: {
      type: String,
      enum: RISK_RATINGS,
      required: true,
    },
  },
  { _id: false }
);

export const ScorecardVersionSchema = new mongoose.Schema<IScorecardVersion, TScorecardVersionModel>(
  {
    scorecardId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: MODEL_NAMES.scorecard,
      required: true,
    },
    version: {
      type: String,
      required: true,
    },
    validFrom: {
      type: Date,
      required: true,
    },
    validTo: {
      type: Date,
    },
    status: {
      type: String,
      enum: SCORECARD_VERSION_STATUSES,
      required: true,
      index: true,
    },
    definition: {
      type: mongoose.Schema.Types.Mixed,
      required: true,
    },
    decisionRules: {
      type: [DecisionRuleSchema],
      required: true,
    },
    scoreBands: {
      type: [ScoreBandSchema],
      required: true,
    },
  },
  {
    timestamps: { createdAt: true, updatedAt: true },
    versionKey: false,
  },
);

export const ScorecardVersion = mongoose.model<IScorecardVersion, TScorecardVersionModel>(MODEL_NAMES.scorecardVersion, ScorecardVersionSchema);

ScorecardVersionSchema.index(
  { scorecardId: 1, version: 1 },
  { unique: true },
);

ScorecardVersionSchema.index(
  { scorecardId: 1, status: 1 },
  { unique: true, partialFilterExpression: { status: 'active' } },
);

ScorecardVersionSchema.pre('save', function(next) {
  if (!this.isNew && this.isModified('definition')) {
    return next(new Error('ScorecardVersion definition is immutable'));
  }
  next();
});
