import {
  GENERATED_OUTPUT_LIMIT_EXPLANATION,
  MAX_GENERATED_OUTPUT_FILES,
} from '../limits';

export interface ResourcePolicy {
  readonly softInputBytes: number;
  readonly hardSingleInputBytes: number;
  readonly hardTotalInputBytes: number;
  readonly softOutputEstimateBytes: number;
  readonly hardOutputEstimateBytes: number;
  readonly maxConcurrentFFmpegJobs: number;
  readonly maxTimelineClips: number;
  readonly maxStreamsPerInput: number;
  readonly maxImageDimension: number;
  readonly maxPixelCount: number;
  readonly maxThumbnailCount: number;
  readonly maxGeneratedOutputs: number;
  readonly maxContactSheetCells: number;
  readonly maxJobRuntimeMs: number;
}

export const DEFAULT_RESOURCE_POLICY: Readonly<ResourcePolicy> = Object.freeze({
  softInputBytes: 256 * 1024 * 1024,
  hardSingleInputBytes: 1536 * 1024 * 1024,
  hardTotalInputBytes: 1536 * 1024 * 1024,
  softOutputEstimateBytes: 512 * 1024 * 1024,
  hardOutputEstimateBytes: 1024 * 1024 * 1024,
  maxConcurrentFFmpegJobs: 1,
  maxTimelineClips: 100,
  maxStreamsPerInput: 64,
  maxImageDimension: 8192,
  maxPixelCount: 8192 * 8192,
  maxThumbnailCount: MAX_GENERATED_OUTPUT_FILES,
  maxGeneratedOutputs: MAX_GENERATED_OUTPUT_FILES,
  maxContactSheetCells: 500,
  maxJobRuntimeMs: 4 * 60 * 60 * 1000,
});

export interface ResourceEstimate {
  readonly inputBytes: readonly number[];
  readonly expectedOutputBytes?: number;
  readonly durationSeconds?: number;
  readonly width?: number;
  readonly height?: number;
  readonly frameRate?: number;
  readonly passes?: number;
  readonly generatedOutputs?: number;
  readonly retainedOutputs?: number;
  readonly timelineClips?: number;
  readonly streamsPerInput?: readonly number[];
  readonly thumbnailCount?: number;
  readonly contactSheetCells?: number;
  readonly expectedRuntimeMs?: number;
}

export type ResourceDecisionStatus = 'allowed' | 'warning' | 'blocked';

export interface ResourceIssue {
  readonly code: string;
  readonly severity: 'warning' | 'error';
  readonly message: string;
  readonly suggestions: readonly string[];
}

export interface ResourceDecision {
  readonly status: ResourceDecisionStatus;
  readonly issues: readonly ResourceIssue[];
  readonly totals: {
    readonly inputBytes: number;
    readonly pixelCount?: number;
    readonly pixelWorkEstimate?: number;
  };
  readonly estimateDisclaimer: string;
}

export function createResourcePolicy(
  overrides: Partial<ResourcePolicy> = {}
): Readonly<ResourcePolicy> {
  const policy = { ...DEFAULT_RESOURCE_POLICY, ...overrides };
  for (const [key, value] of Object.entries(policy)) {
    if (!Number.isFinite(value) || value <= 0) {
      throw new RangeError(`${key} must be a positive finite number`);
    }
  }
  if (policy.softInputBytes > policy.hardSingleInputBytes) {
    throw new RangeError('softInputBytes cannot exceed hardSingleInputBytes');
  }
  if (policy.softOutputEstimateBytes > policy.hardOutputEstimateBytes) {
    throw new RangeError(
      'softOutputEstimateBytes cannot exceed hardOutputEstimateBytes'
    );
  }
  for (const key of [
    'maxConcurrentFFmpegJobs',
    'maxTimelineClips',
    'maxStreamsPerInput',
    'maxImageDimension',
    'maxPixelCount',
    'maxThumbnailCount',
    'maxGeneratedOutputs',
    'maxContactSheetCells',
  ] as const) {
    if (!Number.isSafeInteger(policy[key])) {
      throw new RangeError(`${key} must be a positive safe integer`);
    }
  }
  if (policy.maxThumbnailCount > policy.maxGeneratedOutputs) {
    throw new RangeError('maxThumbnailCount cannot exceed maxGeneratedOutputs');
  }
  return Object.freeze(policy);
}

export function preflightResources(
  estimate: ResourceEstimate,
  policy: ResourcePolicy = DEFAULT_RESOURCE_POLICY
): ResourceDecision {
  const issues: ResourceIssue[] = [];
  validateOptionalNonnegative(
    estimate.expectedOutputBytes,
    'expectedOutputBytes'
  );
  validateOptionalNonnegative(estimate.durationSeconds, 'durationSeconds');
  validateOptionalNonnegative(estimate.expectedRuntimeMs, 'expectedRuntimeMs');
  validateOptionalCount(estimate.thumbnailCount, 'thumbnailCount');
  validateOptionalCount(estimate.contactSheetCells, 'contactSheetCells');
  validateOptionalCount(estimate.retainedOutputs, 'retainedOutputs');
  if (
    estimate.frameRate !== undefined &&
    (!Number.isFinite(estimate.frameRate) ||
      estimate.frameRate <= 0 ||
      estimate.frameRate > 1_000)
  ) {
    throw new RangeError(
      'frameRate must be greater than 0 and no more than 1000'
    );
  }
  if (
    estimate.passes !== undefined &&
    (!Number.isSafeInteger(estimate.passes) ||
      estimate.passes < 1 ||
      estimate.passes > 10)
  ) {
    throw new RangeError('passes must be an integer from 1 to 10');
  }
  if (
    estimate.generatedOutputs !== undefined &&
    (!Number.isSafeInteger(estimate.generatedOutputs) ||
      estimate.generatedOutputs < 1)
  ) {
    throw new RangeError('generatedOutputs must be a positive integer');
  }
  if (
    estimate.timelineClips !== undefined &&
    (!Number.isSafeInteger(estimate.timelineClips) ||
      estimate.timelineClips < 0)
  ) {
    throw new RangeError('timelineClips must be a nonnegative integer');
  }
  if (
    estimate.timelineClips !== undefined &&
    estimate.timelineClips > policy.maxTimelineClips
  ) {
    issues.push(
      issue(
        'timeline-clip-limit',
        'error',
        'The timeline contains more clips than the configured browser safety limit.',
        ['Split the sequence into separate projects or exports']
      )
    );
  }
  for (const [index, streamCount] of (
    estimate.streamsPerInput ?? []
  ).entries()) {
    if (!Number.isSafeInteger(streamCount) || streamCount < 0) {
      throw new RangeError(
        `streamsPerInput.${index} must be a nonnegative integer`
      );
    }
    if (streamCount > policy.maxStreamsPerInput) {
      issues.push(
        issue(
          'stream-count-limit',
          'error',
          `Input ${index + 1} exceeds the configured stream-count safety limit.`,
          ['Remove unnecessary streams in a separate operation']
        )
      );
    }
  }
  const inputBytes = estimate.inputBytes.reduce((sum, value, index) => {
    if (!Number.isSafeInteger(value) || value < 0) {
      throw new RangeError(
        `inputBytes.${index} must be a nonnegative safe integer`
      );
    }
    if (value > policy.hardSingleInputBytes) {
      issues.push(
        issue(
          'single-input-hard-limit',
          'error',
          `Input ${index + 1} exceeds the configured single-file safety limit.`,
          [
            'Trim the source first',
            'Use a desktop FFmpeg workflow for this file',
          ]
        )
      );
    } else if (value > policy.softInputBytes) {
      issues.push(
        issue(
          'input-soft-limit',
          'warning',
          `Input ${index + 1} exceeds the conservative browser processing threshold.`,
          ['Use a lower output resolution', 'Process a shorter section']
        )
      );
    }
    return sum + value;
  }, 0);
  if (inputBytes > policy.hardTotalInputBytes) {
    issues.push(
      issue(
        'total-input-hard-limit',
        'error',
        'Combined source size exceeds the configured browser safety limit.',
        ['Reduce the number of clips', 'Split the work into separate exports']
      )
    );
  }
  if (estimate.expectedOutputBytes !== undefined) {
    if (estimate.expectedOutputBytes > policy.hardOutputEstimateBytes) {
      issues.push(
        issue(
          'output-hard-limit',
          'error',
          'Estimated output size exceeds the configured in-memory output safety limit.',
          ['Lower the bitrate or resolution', 'Export a shorter duration']
        )
      );
    } else if (estimate.expectedOutputBytes > policy.softOutputEstimateBytes) {
      issues.push(
        issue(
          'output-soft-limit',
          'warning',
          'Estimated output size is above the conservative in-memory output threshold.',
          ['Lower the bitrate or resolution', 'Export a shorter duration']
        )
      );
    }
  }
  let pixelCount: number | undefined;
  let pixelWorkEstimate: number | undefined;
  if (estimate.width !== undefined || estimate.height !== undefined) {
    if (
      estimate.width === undefined ||
      estimate.height === undefined ||
      !Number.isFinite(estimate.width) ||
      !Number.isFinite(estimate.height) ||
      estimate.width <= 0 ||
      estimate.height <= 0
    ) {
      throw new RangeError(
        'Both width and height must be positive when provided'
      );
    }
    pixelCount = estimate.width * estimate.height;
    if (
      estimate.width > policy.maxImageDimension ||
      estimate.height > policy.maxImageDimension ||
      pixelCount > policy.maxPixelCount
    ) {
      issues.push(
        issue(
          'pixel-limit',
          'error',
          'Output dimensions exceed the configured image or pixel limit.',
          ['Choose a lower output resolution']
        )
      );
    }
    if (
      estimate.durationSeconds !== undefined &&
      estimate.frameRate !== undefined
    ) {
      pixelWorkEstimate =
        pixelCount *
        estimate.durationSeconds *
        estimate.frameRate *
        (estimate.passes ?? 1);
    }
  }
  if (
    estimate.thumbnailCount !== undefined &&
    estimate.thumbnailCount > policy.maxThumbnailCount
  ) {
    issues.push(
      issue(
        'thumbnail-limit',
        'error',
        `Requested thumbnail count exceeds the configured ${policy.maxThumbnailCount}-image limit. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`,
        ['Increase the interval or reduce the count']
      )
    );
  }
  if (
    estimate.contactSheetCells !== undefined &&
    estimate.contactSheetCells > policy.maxContactSheetCells
  ) {
    issues.push(
      issue(
        'contact-sheet-limit',
        'error',
        'Requested contact-sheet cell count exceeds the configured limit.',
        ['Use fewer rows or columns']
      )
    );
  }
  if (
    estimate.generatedOutputs !== undefined &&
    estimate.generatedOutputs + (estimate.retainedOutputs ?? 0) >
      policy.maxGeneratedOutputs
  ) {
    const requestedTotal =
      estimate.generatedOutputs + (estimate.retainedOutputs ?? 0);
    issues.push(
      issue(
        'generated-output-limit',
        'error',
        `This operation would retain ${requestedTotal} generated files; the managed result collection allows at most ${policy.maxGeneratedOutputs}. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`,
        [
          'Save or remove existing results before starting this operation',
          'Generate fewer outputs in this batch',
        ]
      )
    );
  }
  if (
    estimate.expectedRuntimeMs !== undefined &&
    estimate.expectedRuntimeMs > policy.maxJobRuntimeMs
  ) {
    issues.push(
      issue(
        'runtime-limit',
        'error',
        'Estimated runtime exceeds the configured maximum job duration.',
        ['Use a shorter clip', 'Choose a faster preset']
      )
    );
  }
  const hasErrors = issues.some((entry) => entry.severity === 'error');
  return Object.freeze({
    status: hasErrors ? 'blocked' : issues.length > 0 ? 'warning' : 'allowed',
    issues: Object.freeze(issues),
    totals: Object.freeze({
      inputBytes,
      ...(pixelCount !== undefined ? { pixelCount } : {}),
      ...(pixelWorkEstimate !== undefined ? { pixelWorkEstimate } : {}),
    }),
    estimateDisclaimer:
      'Resource estimates are conservative guidance, not a guarantee of browser memory use or completion time.',
  });
}

export function estimateOutputBytes(
  durationSeconds: number,
  videoBitrateKbps = 0,
  audioBitrateKbps = 0,
  overheadFraction = 0.03
): number {
  if (
    !Number.isFinite(durationSeconds) ||
    durationSeconds < 0 ||
    !Number.isFinite(videoBitrateKbps) ||
    videoBitrateKbps < 0 ||
    !Number.isFinite(audioBitrateKbps) ||
    audioBitrateKbps < 0 ||
    !Number.isFinite(overheadFraction) ||
    overheadFraction < 0 ||
    overheadFraction > 1
  ) {
    throw new RangeError(
      'Output estimate inputs are outside their supported ranges'
    );
  }
  return Math.ceil(
    (((videoBitrateKbps + audioBitrateKbps) * 1000 * durationSeconds) / 8) *
      (1 + overheadFraction)
  );
}

export function policyForDeviceMemory(
  policy: ResourcePolicy,
  deviceMemoryGiB?: number
): Readonly<ResourcePolicy> {
  if (
    deviceMemoryGiB === undefined ||
    !Number.isFinite(deviceMemoryGiB) ||
    deviceMemoryGiB >= 4
  ) {
    return Object.freeze({ ...policy });
  }
  const factor = deviceMemoryGiB <= 2 ? 0.35 : 0.6;
  return createResourcePolicy({
    ...policy,
    softInputBytes: Math.max(
      32 * 1024 * 1024,
      Math.floor(policy.softInputBytes * factor)
    ),
    hardSingleInputBytes: Math.max(
      128 * 1024 * 1024,
      Math.floor(policy.hardSingleInputBytes * factor)
    ),
    hardTotalInputBytes: Math.max(
      128 * 1024 * 1024,
      Math.floor(policy.hardTotalInputBytes * factor)
    ),
    softOutputEstimateBytes: Math.max(
      64 * 1024 * 1024,
      Math.floor(policy.softOutputEstimateBytes * factor)
    ),
    hardOutputEstimateBytes: Math.max(
      128 * 1024 * 1024,
      Math.floor(policy.hardOutputEstimateBytes * factor)
    ),
  });
}

function issue(
  code: string,
  severity: ResourceIssue['severity'],
  message: string,
  suggestions: readonly string[]
): ResourceIssue {
  return Object.freeze({
    code,
    severity,
    message,
    suggestions: Object.freeze([...suggestions]),
  });
}

function validateOptionalNonnegative(
  value: number | undefined,
  label: string
): void {
  if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
    throw new RangeError(`${label} must be a finite nonnegative number`);
  }
}

function validateOptionalCount(value: number | undefined, label: string): void {
  if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
    throw new RangeError(`${label} must be a nonnegative safe integer`);
  }
}
