import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
{{#each dynamic_imports}}
{{{this}}}
{{/each}}

{{#each dynamic_globals}}
{{{this}}}
{{/each}}

// Flow-level metrics
const flowSuccessRate = new Rate('flow_success_rate');
const flowDuration = new Trend('flow_duration');
const flowErrors = new Counter('flow_errors');
{{#if error_injection_enabled}}
// Error injection metrics
const errorInjectionRate = new Rate('error_injection_rate');
const errorInjectionCount = new Counter('error_injection_count');
{{/if}}

// Step-level metrics
{{#each flows}}
{{#each this.steps}}
const {{../name}}_step{{@index}}_latency = new Trend('{{../name}}_step{{@index}}_latency');
const {{../name}}_step{{@index}}_errors = new Rate('{{../name}}_step{{@index}}_errors');
{{/each}}
{{/each}}

// Test configuration
export const options = {
  {{#if skip_tls_verify}}
  insecureSkipTLSVerify: true,
  {{/if}}
  scenarios: {
    crud_flow: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        {{#each stages}}
        { duration: '{{this.duration}}', target: {{this.target}} },
        {{/each}}
      ],
      gracefulRampDown: '10s',
    },
  },
  thresholds: {
    'http_req_duration': ['{{threshold_percentile}}<{{threshold_ms}}'],
    'http_req_failed': ['rate<{{max_error_rate}}'],
    'flow_success_rate': ['rate>0.95'],
  },
};

const BASE_URL = '{{base_url}}';
const EXTRACTED_VALUES_OUTPUT_PATH = '{{extracted_values_output_path}}';

// Common headers
const headers = {{{headers}}};
const globalExtractedValues = {};

{{#if error_injection_enabled}}
// Error injection configuration
const ERROR_RATE = {{error_rate}};
const ERROR_TYPES = [{{#each error_types}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}];

// Determine if this request should be invalidated
function shouldInvalidate() {
  const inject = Math.random() < ERROR_RATE;
  errorInjectionRate.add(inject);
  return inject;
}

// Select a random error type
function selectErrorType() {
  if (ERROR_TYPES.length === 0) {
    return 'MissingField'; // Default
  }
  return ERROR_TYPES[Math.floor(Math.random() * ERROR_TYPES.length)];
}

// Apply error injection to a payload
function injectError(payload) {
  if (!payload || typeof payload !== 'object') {
    return payload;
  }

  const errorType = selectErrorType();
  const result = { ...payload };
  const keys = Object.keys(result);

  if (keys.length === 0) {
    return result;
  }

  const randomKey = keys[Math.floor(Math.random() * keys.length)];
  const originalValue = result[randomKey];

  switch (errorType) {
    case 'MissingField':
      // Remove a required field
      delete result[randomKey];
      console.log(`[ERROR INJECTION] Removed field: ${randomKey}`);
      break;

    case 'WrongType':
      // Change the type of a field
      if (typeof originalValue === 'string') {
        result[randomKey] = 12345;
      } else if (typeof originalValue === 'number') {
        result[randomKey] = 'invalid_string';
      } else if (typeof originalValue === 'boolean') {
        result[randomKey] = 'not_a_boolean';
      } else if (Array.isArray(originalValue)) {
        result[randomKey] = 'not_an_array';
      } else {
        result[randomKey] = null;
      }
      console.log(`[ERROR INJECTION] Changed type of ${randomKey}`);
      break;

    case 'Null':
      // Set field to null
      result[randomKey] = null;
      console.log(`[ERROR INJECTION] Set ${randomKey} to null`);
      break;

    case 'OutOfRange':
      // Set numeric fields to extreme values
      if (typeof originalValue === 'number') {
        result[randomKey] = originalValue > 0 ? -999999999 : 999999999;
      } else if (typeof originalValue === 'string') {
        result[randomKey] = 'x'.repeat(10000); // Very long string
      }
      console.log(`[ERROR INJECTION] Set ${randomKey} to out-of-range value`);
      break;

    case 'Empty':
      // Set field to empty value
      if (typeof originalValue === 'string') {
        result[randomKey] = '';
      } else if (Array.isArray(originalValue)) {
        result[randomKey] = [];
      } else if (typeof originalValue === 'object') {
        result[randomKey] = {};
      }
      console.log(`[ERROR INJECTION] Set ${randomKey} to empty`);
      break;

    case 'InvalidFormat':
      // Set field to invalid format
      if (typeof originalValue === 'string') {
        // Inject special characters
        result[randomKey] = '!@#$%^&*()_+{}|:"<>?';
      }
      console.log(`[ERROR INJECTION] Set ${randomKey} to invalid format`);
      break;

    default:
      // Default: remove the field
      delete result[randomKey];
      console.log(`[ERROR INJECTION] Default: removed field ${randomKey}`);
  }

  errorInjectionCount.add(1);
  return result;
}
{{/if}}

// Helper function to extract value from JSON response
// Supports: simple fields (uuid), nested paths (data.uuid),
// array indices (results[0].uuid), and filters (results[?name="global"].uuid)
function extractValue(response, fieldPath, matchMode) {
  try {
    const json = response.json();
    return extractFromObject(json, fieldPath, matchMode || 'first');
  } catch (e) {
    console.error(`Failed to extract ${fieldPath}: ${e.message}`);
    return null;
  }
}

// Extract value from object using path expression
function extractFromObject(obj, fieldPath, matchMode) {
  if (!obj || !fieldPath) return null;

  // Parse the field path into segments
  const segments = parseFieldPath(fieldPath);
  let value = obj;

  for (const segment of segments) {
    if (value === null || value === undefined) return null;

    if (segment.type === 'field') {
      // Simple field access
      value = value[segment.name];
    } else if (segment.type === 'index') {
      // Array index access: [0], [1], etc.
      if (!Array.isArray(value)) return null;
      const idx = segment.index < 0 ? value.length + segment.index : segment.index;
      value = value[idx];
    } else if (segment.type === 'filter') {
      // Filter access: [?field="value"] or [?field=value]
      if (!Array.isArray(value)) return null;
      const matches = value.filter(item => {
        if (item && typeof item === 'object') {
          const fieldVal = item[segment.filterField];
          return String(fieldVal) === String(segment.filterValue);
        }
        return false;
      });
      if (matches.length === 0) return null;
      // Return first or last based on matchMode
      value = matchMode === 'last' ? matches[matches.length - 1] : matches[0];
    }
  }

  return value;
}

// Parse field path into segments
function parseFieldPath(fieldPath) {
  const segments = [];
  let remaining = fieldPath;

  while (remaining.length > 0) {
    // Check for array index or filter: [0] or [?name="value"]
    if (remaining.startsWith('[')) {
      const closeIdx = remaining.indexOf(']');
      if (closeIdx === -1) break;

      const bracketContent = remaining.substring(1, closeIdx);

      if (bracketContent.startsWith('?')) {
        // Filter expression: [?name="value"] or [?name=value]
        const filterExpr = bracketContent.substring(1);
        const eqIdx = filterExpr.indexOf('=');
        if (eqIdx !== -1) {
          const filterField = filterExpr.substring(0, eqIdx).trim();
          let filterValue = filterExpr.substring(eqIdx + 1).trim();
          // Remove quotes if present
          if ((filterValue.startsWith('"') && filterValue.endsWith('"')) ||
              (filterValue.startsWith("'") && filterValue.endsWith("'"))) {
            filterValue = filterValue.substring(1, filterValue.length - 1);
          }
          segments.push({ type: 'filter', filterField, filterValue });
        }
      } else {
        // Numeric index: [0], [-1], etc.
        const index = parseInt(bracketContent, 10);
        if (!isNaN(index)) {
          segments.push({ type: 'index', index });
        }
      }

      remaining = remaining.substring(closeIdx + 1);
      // Skip leading dot after bracket
      if (remaining.startsWith('.')) {
        remaining = remaining.substring(1);
      }
    } else {
      // Field name
      let dotIdx = remaining.indexOf('.');
      let bracketIdx = remaining.indexOf('[');

      let endIdx;
      if (dotIdx === -1 && bracketIdx === -1) {
        endIdx = remaining.length;
      } else if (dotIdx === -1) {
        endIdx = bracketIdx;
      } else if (bracketIdx === -1) {
        endIdx = dotIdx;
      } else {
        endIdx = Math.min(dotIdx, bracketIdx);
      }

      const fieldName = remaining.substring(0, endIdx);
      if (fieldName.length > 0) {
        segments.push({ type: 'field', name: fieldName });
      }

      remaining = remaining.substring(endIdx);
      // Skip leading dot
      if (remaining.startsWith('.')) {
        remaining = remaining.substring(1);
      }
    }
  }

  return segments;
}

// Helper function to extract full response body with optional key filtering
function extractBody(response, excludeKeys) {
  try {
    const json = response.json();
    if (excludeKeys && excludeKeys.length > 0) {
      const filtered = { ...json };
      for (const key of excludeKeys) {
        delete filtered[key];
      }
      return filtered;
    }
    return json;
  } catch (e) {
    console.error(`Failed to extract body: ${e.message}`);
    return null;
  }
}

// Helper function to deep merge objects (target is modified with overrides)
function mergeObjects(target, overrides) {
  if (!target || typeof target !== 'object') return overrides;
  if (!overrides || typeof overrides !== 'object') return target;

  const result = { ...target };
  for (const [key, value] of Object.entries(overrides)) {
    if (value && typeof value === 'object' && !Array.isArray(value) &&
        result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
      // Deep merge for nested objects
      result[key] = mergeObjects(result[key], value);
    } else {
      // Direct override for primitives and arrays
      result[key] = value;
    }
  }
  return result;
}

// Helper function to replace path parameters
function replacePath(pathTemplate, values) {
  let path = pathTemplate;
  for (const [key, value] of Object.entries(values)) {
    path = path.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
  }
  return path;
}

export default function () {
  {{#each flows}}
  // Flow: {{this.display_name}}
  group('{{this.display_name}}', function() {
    const flowStart = Date.now();
    let flowSuccess = true;
    const extractedValues = {};

    {{#each this.steps}}
    // Step {{@index}}: {{this.description}}
    {
      {{#if this.use_values}}
      // Apply extracted values to path
      const pathValues = {};
      {{#each this.use_values}}
      if (extractedValues['{{this}}']) {
        pathValues['{{@key}}'] = extractedValues['{{this}}'];
      }
      {{/each}}
      let path = replacePath('{{{this.path}}}', pathValues);
      {{else}}
      let path = '{{{this.path}}}';
      {{/if}}

      {{#if @root.security_testing_enabled}}
      // Security testing: get next payload group and apply to request
      const secPayloadGroup = typeof getNextSecurityPayload === 'function' ? getNextSecurityPayload() : [];
      let requestHeaders = { ...headers };
      let secBodyPayload = null;
      let hasSecCookie = false;
      for (const secPayload of secPayloadGroup) {
        if (secPayload.location === 'header' && secPayload.headerName) {
          requestHeaders[secPayload.headerName] = secPayload.payload;
          if (secPayload.headerName === 'Cookie') hasSecCookie = true;
        }
        if (secPayload.location === 'uri') {
          if (secPayload.injectAsPath) {
            // Replace URL path so WAF inspects via REQUEST_FILENAME (e.g., CRS 942101)
            path = `/${encodeURI(secPayload.payload)}`;
          } else {
            const separator = path.includes('?') ? '&' : '?';
            path = `${path}${separator}test=${encodeURIComponent(secPayload.payload)}`;
          }
        }
        if (secPayload.location === 'body') {
          secBodyPayload = secPayload;
        }
      }
      // Skip CookieJar when security payload sets Cookie header (jar overrides manual Cookie headers)
      const secRequestOpts = hasSecCookie ? { headers: requestHeaders } : { headers: requestHeaders, jar: new http.CookieJar() };
      {{/if}}

      {{#if this.use_body}}
      // Use previously extracted body
      let payload = extractedValues['{{this.use_body}}'];
      if (!payload) {
        console.error('Extracted body {{this.use_body}} not found');
        payload = {};
      }
      {{#if this.merge_body}}
      // Merge with override values
      payload = mergeObjects(payload, {{{json this.merge_body}}});
      {{/if}}
      {{#if @root.security_testing_enabled}}
      // Apply security payload to body if available
      let requestBody;
      if (secBodyPayload && secBodyPayload.formBody) {
        // Parse form body into object for k6's native form encoding (CRS 942432)
        const formData = {};
        secBodyPayload.formBody.split('&').forEach(function(pair) {
          const eq = pair.indexOf('=');
          if (eq >= 0) {
            formData[decodeURIComponent(pair.substring(0, eq).replace(/\+/g, ' '))] =
              decodeURIComponent(pair.substring(eq + 1).replace(/\+/g, ' '));
          }
        });
        requestBody = formData;
      } else if (secBodyPayload && typeof applySecurityPayload === 'function') {
        payload = applySecurityPayload(payload, [], secBodyPayload);
        requestBody = JSON.stringify(payload);
      } else {
        requestBody = JSON.stringify(payload);
      }
      {{/if}}
      {{#if @root.error_injection_enabled}}
      // Apply error injection if triggered
      if (shouldInvalidate()) {
        payload = injectError(payload);
      }
      {{/if}}
      {{#if @root.security_testing_enabled}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, requestBody, secRequestOpts);
      {{else}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, JSON.stringify(payload), { headers, jar: new http.CookieJar() });
      {{/if}}
      {{else if this.has_body}}
      {{#if this.body_is_dynamic}}
      // Dynamic body with runtime placeholders and extracted values
      let payload = {{{this.body}}};
      {{#each this.use_values}}
      // Replace {{@key}} with extracted value if present
      if (extractedValues['{{this}}']) {
        payload = JSON.parse(JSON.stringify(payload).replace(/\$\{extracted\.{{@key}}\}/g, extractedValues['{{this}}']));
      }
      {{/each}}
      {{else}}
      let payload = {{{this.body}}};
      {{/if}}
      {{#if @root.security_testing_enabled}}
      // Apply security payload to body if available
      let requestBody;
      if (secBodyPayload && secBodyPayload.formBody) {
        // Parse form body into object for k6's native form encoding (CRS 942432)
        const formData = {};
        secBodyPayload.formBody.split('&').forEach(function(pair) {
          const eq = pair.indexOf('=');
          if (eq >= 0) {
            formData[decodeURIComponent(pair.substring(0, eq).replace(/\+/g, ' '))] =
              decodeURIComponent(pair.substring(eq + 1).replace(/\+/g, ' '));
          }
        });
        requestBody = formData;
      } else if (secBodyPayload && typeof applySecurityPayload === 'function') {
        payload = applySecurityPayload(payload, [], secBodyPayload);
        requestBody = JSON.stringify(payload);
      } else {
        requestBody = JSON.stringify(payload);
      }
      {{/if}}
      {{#if @root.error_injection_enabled}}
      // Apply error injection if triggered
      if (shouldInvalidate()) {
        payload = injectError(payload);
      }
      {{/if}}
      {{#if @root.security_testing_enabled}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, requestBody, secRequestOpts);
      {{else}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, JSON.stringify(payload), { headers, jar: new http.CookieJar() });
      {{/if}}
      {{else if this.is_get_or_head}}
      {{#if @root.security_testing_enabled}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, secRequestOpts);
      {{else}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, { headers, jar: new http.CookieJar() });
      {{/if}}
      {{else}}
      {{#if @root.security_testing_enabled}}
      // Send form-encoded body if available, otherwise null
      let requestBody = null;
      if (secBodyPayload && secBodyPayload.formBody) {
        // Parse form body into object for k6's native form encoding (CRS 942432)
        const formData = {};
        secBodyPayload.formBody.split('&').forEach(function(pair) {
          const eq = pair.indexOf('=');
          if (eq >= 0) {
            formData[decodeURIComponent(pair.substring(0, eq).replace(/\+/g, ' '))] =
              decodeURIComponent(pair.substring(eq + 1).replace(/\+/g, ' '));
          }
        });
        requestBody = formData;
      }
      const res = http.{{this.method}}(`${BASE_URL}${path}`, requestBody, secRequestOpts);
      {{else}}
      const res = http.{{this.method}}(`${BASE_URL}${path}`, null, { headers, jar: new http.CookieJar() });
      {{/if}}
      {{/if}}

      const success = check(res, {
        '{{this.display_name}}: status is OK': (r) => r.status >= 200 && r.status < 300,
      });

      {{../name}}_step{{@index}}_latency.add(res.timings.duration);
      {{../name}}_step{{@index}}_errors.add(!success);

      if (!success) {
        flowSuccess = false;
        console.error(`Step {{@index}} ({{this.display_name}}) failed: ${res.status} - ${res.body}`);
      }

      {{#if this.extract}}
      // Extract values for subsequent steps
      if (success) {
        {{#each this.extract}}
        {{#if this.body}}
        // Extract full response body with filtering
        const extracted_{{this.store_as}} = extractBody(res, {{{json this.exclude}}});
        {{else}}
        // Extract field: {{{this.field}}}{{#if this.match_mode}} (match: {{this.match_mode}}){{/if}}
        const extracted_{{this.store_as}} = extractValue(res, '{{{this.field}}}'{{#if this.match_mode}}, '{{this.match_mode}}'{{/if}});
        {{/if}}
        if (extracted_{{this.store_as}} !== null && extracted_{{this.store_as}} !== undefined) {
          extractedValues['{{this.store_as}}'] = extracted_{{this.store_as}};
          globalExtractedValues['{{this.store_as}}'] = extracted_{{this.store_as}};
        }
        {{/each}}
      }
      {{/if}}

      sleep(0.5); // Brief pause between steps
    }
    {{/each}}

    const flowEnd = Date.now();
    flowDuration.add(flowEnd - flowStart);
    flowSuccessRate.add(flowSuccess);
    if (!flowSuccess) {
      flowErrors.add(1);
    }
  });
  {{/each}}

  sleep(1); // Pause between flow iterations
}

export function handleSummary(data) {
  return {
    [EXTRACTED_VALUES_OUTPUT_PATH]: JSON.stringify(globalExtractedValues, null, 2),
    'stdout': textSummary(data, { indent: ' ', enableColors: true }),
  };
}

function textSummary(data, options) {
  const indent = options.indent || '';
  const enableColors = options.enableColors || false;

  const metrics = data.metrics;
  let output = '\n';

  output += indent + '='.repeat(60) + '\n';
  output += indent + 'CRUD Flow Test Summary\n';
  output += indent + '='.repeat(60) + '\n\n';

  // Flow metrics
  if (metrics.flow_success_rate && metrics.flow_success_rate.values) {
    const rate = metrics.flow_success_rate.values.rate;
    const successRate = rate != null ? (rate * 100).toFixed(2) : 'N/A';
    output += indent + `Flow Success Rate: ${successRate}%\n`;
  }

  if (metrics.flow_duration && metrics.flow_duration.values) {
    const v = metrics.flow_duration.values;
    output += indent + `Flow Duration (avg): ${v.avg != null ? v.avg.toFixed(2) : 'N/A'}ms\n`;
    output += indent + `Flow Duration (p95): ${v['p(95)'] != null ? v['p(95)'].toFixed(2) : 'N/A'}ms\n`;
  }

  if (metrics.flow_errors && metrics.flow_errors.values) {
    const count = metrics.flow_errors.values.count || 0;
    output += indent + `Flow Errors: ${count}\n`;
  }

  // Error injection metrics (if enabled)
  if (metrics.error_injection_count && metrics.error_injection_count.values) {
    const injected = metrics.error_injection_count.values.count || 0;
    output += indent + `Error Injections: ${injected}\n`;
  }
  if (metrics.error_injection_rate && metrics.error_injection_rate.values) {
    const rate = metrics.error_injection_rate.values.rate;
    const injectionRate = rate != null ? (rate * 100).toFixed(2) : '0.00';
    output += indent + `Error Injection Rate: ${injectionRate}%\n`;
  }

  output += '\n';

  // Request metrics
  if (metrics.http_reqs && metrics.http_reqs.values) {
    const count = metrics.http_reqs.values.count || 0;
    const rate = metrics.http_reqs.values.rate;
    output += indent + `Total Requests: ${count}\n`;
    output += indent + `Request Rate: ${rate != null ? rate.toFixed(2) : '0.00'} req/s\n\n`;
  }

  // Duration metrics
  if (metrics.http_req_duration && metrics.http_req_duration.values) {
    const v = metrics.http_req_duration.values;
    output += indent + 'Response Times:\n';
    output += indent + `  Min: ${v.min != null ? v.min.toFixed(2) : 'N/A'}ms\n`;
    output += indent + `  Avg: ${v.avg != null ? v.avg.toFixed(2) : 'N/A'}ms\n`;
    output += indent + `  Med: ${v.med != null ? v.med.toFixed(2) : 'N/A'}ms\n`;
    output += indent + `  p90: ${v['p(90)'] != null ? v['p(90)'].toFixed(2) : 'N/A'}ms\n`;
    output += indent + `  p95: ${v['p(95)'] != null ? v['p(95)'].toFixed(2) : 'N/A'}ms\n`;
    output += indent + `  p99: ${v['p(99)'] != null ? v['p(99)'].toFixed(2) : 'N/A'}ms\n`;
    output += indent + `  Max: ${v.max != null ? v.max.toFixed(2) : 'N/A'}ms\n\n`;
  }

  // Error rate
  if (metrics.http_req_failed && metrics.http_req_failed.values) {
    const rate = metrics.http_req_failed.values.rate;
    const errorRate = rate != null ? (rate * 100).toFixed(2) : '100.00';
    output += indent + `Error Rate: ${errorRate}%\n\n`;
  }

  output += indent + '='.repeat(60) + '\n';

  return output;
}
