import { Injectable } from '@nestjs/common';
import { readFile } from 'node:fs/promises';

export type OperationalMode = 'normal' | 'maintenance' | 'test';
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';
export type ActionType = 'view' | 'mutate';

export type PolicySnapshot = {
  readonly policyVersion: number;
  readonly operationalMode: OperationalMode;
  readonly maintenanceAllowlist: {
    readonly view: readonly string[];
    readonly mutate: readonly string[];
  };
  readonly capabilities: Readonly<Record<string, FeatureState>>;
  readonly loadedFrom: 'env' | 'file' | 'defaults';
  readonly loadedAtIso: string;
};

const DEFAULT_POLICY_VERSION = 1;
const DEFAULT_CACHE_MS = 5_000;

type RawPolicySnapshot = Partial<{
  policyVersion: number;
  operationalMode: string;
  maintenanceAllowlist: Partial<{
    view: unknown;
    mutate: unknown;
  }>;
  capabilities: unknown;
}>;

@Injectable()
export class PolicyService {
  private cache:
    | {
        snapshot: PolicySnapshot;
        expiresAtMs: number;
      }
    | null = null;

  async getSnapshot(): Promise<PolicySnapshot> {
    const now = Date.now();
    if (this.cache && now < this.cache.expiresAtMs) {
      return this.cache.snapshot;
    }

    const cacheMs = parseCacheMs(process.env.GRIDPILOT_POLICY_CACHE_MS);
    const loadedAtIso = new Date(now).toISOString();
    const { raw, loadedFrom } = await this.loadRawSnapshot();
    const snapshot = normalizeSnapshot(raw, loadedFrom, loadedAtIso);

    this.cache = {
      snapshot,
      expiresAtMs: now + cacheMs,
    };

    return snapshot;
  }

  private async loadRawSnapshot(): Promise<{
    raw: RawPolicySnapshot;
    loadedFrom: PolicySnapshot['loadedFrom'];
  }> {
    const policyPath = process.env.GRIDPILOT_POLICY_PATH;

    if (policyPath) {
      const rawJson = await readFile(policyPath, 'utf8');
      return {
        raw: JSON.parse(rawJson) as RawPolicySnapshot,
        loadedFrom: 'file',
      };
    }

    const anyEnvConfigured =
      Boolean(process.env.GRIDPILOT_OPERATIONAL_MODE) ||
      Boolean(process.env.GRIDPILOT_FEATURES_JSON) ||
      Boolean(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW) ||
      Boolean(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);

    const raw: RawPolicySnapshot = {};

    const operationalMode = process.env.GRIDPILOT_OPERATIONAL_MODE;
    if (operationalMode) {
      raw.operationalMode = operationalMode;
    }

    const capabilities = parseFeaturesJson(process.env.GRIDPILOT_FEATURES_JSON);
    if (capabilities) {
      raw.capabilities = capabilities;
    }

    const maintenanceAllowView = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW);
    const maintenanceAllowMutate = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);

    if (maintenanceAllowView || maintenanceAllowMutate) {
      raw.maintenanceAllowlist = {
        ...(maintenanceAllowView ? { view: maintenanceAllowView } : {}),
        ...(maintenanceAllowMutate ? { mutate: maintenanceAllowMutate } : {}),
      };
    }

    return {
      raw,
      loadedFrom: anyEnvConfigured ? 'env' : 'defaults',
    };
  }
}

function normalizeSnapshot(
  raw: RawPolicySnapshot,
  loadedFrom: PolicySnapshot['loadedFrom'],
  loadedAtIso: string,
): PolicySnapshot {
  const operationalMode = parseOperationalMode(raw.operationalMode);

  const maintenanceAllowlistView = normalizeStringArray(raw.maintenanceAllowlist?.view);
  const maintenanceAllowlistMutate = normalizeStringArray(raw.maintenanceAllowlist?.mutate);

  const capabilities = normalizeCapabilities(raw.capabilities);

  return {
    policyVersion:
      typeof raw.policyVersion === 'number' ? raw.policyVersion : DEFAULT_POLICY_VERSION,
    operationalMode,
    maintenanceAllowlist: {
      view: maintenanceAllowlistView,
      mutate: maintenanceAllowlistMutate,
    },
    capabilities,
    loadedFrom,
    loadedAtIso,
  };
}

function parseCacheMs(cacheMsRaw: string | undefined): number {
  if (!cacheMsRaw) {
    return DEFAULT_CACHE_MS;
  }
  const parsed = Number(cacheMsRaw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return DEFAULT_CACHE_MS;
  }
  return parsed;
}

function parseOperationalMode(raw: string | undefined): OperationalMode {
  switch (raw) {
    case 'normal':
    case 'maintenance':
    case 'test':
      return raw;
    default:
      return 'normal';
  }
}

function normalizeCapabilities(input: unknown): Readonly<Record<string, FeatureState>> {
  if (!input || typeof input !== 'object') {
    return {};
  }

  const record = input as Record<string, unknown>;
  const normalized: Record<string, FeatureState> = {};

  for (const [key, value] of Object.entries(record)) {
    if (!key) {
      continue;
    }
    const state = parseFeatureState(value);
    if (state) {
      normalized[key] = state;
    }
  }

  return normalized;
}

function parseFeatureState(value: unknown): FeatureState | null {
  if (typeof value !== 'string') {
    return null;
  }
  switch (value) {
    case 'enabled':
    case 'disabled':
    case 'coming_soon':
    case 'hidden':
      return value;
    default:
      return null;
  }
}

function parseFeaturesJson(raw: string | undefined): unknown {
  if (!raw) {
    return undefined;
  }
  try {
    return JSON.parse(raw) as unknown;
  } catch {
    return undefined;
  }
}

function parseCsvList(raw: string | undefined): readonly string[] | undefined {
  if (!raw) {
    return undefined;
  }

  const items = raw
    .split(',')
    .map((v) => v.trim())
    .filter(Boolean);

  return items.length > 0 ? items : undefined;
}

function normalizeStringArray(value: unknown): readonly string[] {
  if (!Array.isArray(value)) {
    return [];
  }

  const normalized = value
    .filter((v) => typeof v === 'string')
    .map((v) => v.trim())
    .filter(Boolean);

  return normalized;
}