import { resolveTokenFromStorage } from '../../session';
import type {
  Bilingual,
  FormSectionConfig,
  LicenseApplication,
  LicenseStatus,
  ValidationIssue,
} from './licensing.types';

const BASE_API_URL =
  (import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
  'http://localhost:3001/api';

/**
 * Uploads a document straight to the API.
 *
 * Files go up the moment they're picked rather than being held until submit —
 * the application already exists as a draft, so there is a real owner to
 * attach them to, and nothing can be lost if the browser closes mid-wizard.
 */
export async function uploadDocument(params: {
  ownerType:
    | 'APPLICATION'
    | 'APPLICATION_STAFF'
    | 'INSPECTION'
    | 'LICENSE'
    | 'SEA_SERVICE_RECORD'
    | 'MEDICAL_CERTIFICATE';
  ownerId: string;
  documentKey: string;
  file: File;
  title?: string;
  validFrom?: string;
  validTo?: string;
}): Promise<{ ok: true } | { ok: false; error: string }> {
  const form = new FormData();
  form.append('ownerType', params.ownerType);
  form.append('ownerId', params.ownerId);
  form.append('documentKey', params.documentKey);
  if (params.title) form.append('title', params.title);
  if (params.validFrom) form.append('validFrom', params.validFrom);
  if (params.validTo) form.append('validTo', params.validTo);
  form.append('files', params.file, params.file.name);

  const token = resolveTokenFromStorage();
  const res = await fetch(`${BASE_API_URL}/attachments`, {
    method: 'POST',
    headers: token ? { Authorization: `Bearer ${token}` } : {},
    body: form,
  });
  if (res.ok) return { ok: true };
  const body = await res.json().catch(() => null);
  return { ok: false, error: body?.message ?? `Upload failed (${res.status})` };
}

/** Human label for a status, in the vocabulary the user stories use. */
export const STATUS_LABELS: Record<LicenseStatus, string> = {
  DRAFT: 'Draft',
  SUBMITTED: 'Submitted',
  UNDER_REVIEW: 'Under Review',
  UNDER_EVALUATION: 'Under Evaluation',
  RESUBMIT_REQUIRED: 'Resubmit Required',
  INSPECTION_PENDING: 'Inspection Pending',
  INSPECTION_COMPLETED: 'Inspection Completed',
  APPROVED: 'Approved',
  REJECTED: 'Rejected',
  ON_HOLD: 'On Hold',
  PAYMENT_PENDING: 'Payment Pending',
  PAID: 'Paid',
  PAYMENT_CONFIRMED: 'Preparing Certificate',
  CERTIFICATE_ISSUED: 'Certificate Issued',
  COMPLETED: 'Completed',
};

/** Mantine colour per status — green progresses, orange needs the applicant. */
export const STATUS_COLORS: Record<LicenseStatus, string> = {
  DRAFT: 'gray',
  SUBMITTED: 'blue',
  UNDER_REVIEW: 'indigo',
  UNDER_EVALUATION: 'indigo',
  RESUBMIT_REQUIRED: 'orange',
  INSPECTION_PENDING: 'cyan',
  INSPECTION_COMPLETED: 'cyan',
  APPROVED: 'teal',
  REJECTED: 'red',
  ON_HOLD: 'gray',
  PAYMENT_PENDING: 'yellow',
  PAID: 'lime',
  PAYMENT_CONFIRMED: 'teal',
  CERTIFICATE_ISSUED: 'green',
  COMPLETED: 'green',
};

/**
 * Roughly how far through the lifecycle a status sits, as a percentage.
 *
 * Used only to draw a progress bar. The two terminal outcomes both read as
 * finished — a rejection is the end of the road, not 60% of the way somewhere.
 */
export const STATUS_PROGRESS: Record<LicenseStatus, number> = {
  DRAFT: 5,
  SUBMITTED: 15,
  UNDER_REVIEW: 30,
  UNDER_EVALUATION: 45,
  RESUBMIT_REQUIRED: 30,
  INSPECTION_PENDING: 55,
  INSPECTION_COMPLETED: 65,
  APPROVED: 75,
  // Parked, so it keeps the progress of wherever it was held from.
  ON_HOLD: 45,
  PAYMENT_PENDING: 80,
  PAID: 88,
  PAYMENT_CONFIRMED: 94,
  CERTIFICATE_ISSUED: 100,
  COMPLETED: 100,
  REJECTED: 100,
};

/** Statuses where nothing moves until the applicant does something. */
export const APPLICANT_ACTION_STATUSES: LicenseStatus[] = [
  'DRAFT',
  'RESUBMIT_REQUIRED',
  'PAYMENT_PENDING',
];

/** Statuses that are finished, whichever way they went. */
export const TERMINAL_STATUSES: LicenseStatus[] = [
  'CERTIFICATE_ISSUED',
  'COMPLETED',
  'REJECTED',
];

/** Licence types with no `companyName` — these are filed by a person, not a
 * business, so display falls back to the applicant name captured in the form. */
export const APPLICANT_NAME_TYPE_KEYS = [
  'SEAFARER_REGISTRATION',
  'CERTIFICATE_OF_COMPETENCY',
  'CERTIFICATE_OF_PROFICIENCY',
  'VESSEL_REGISTRATION',
  'VESSEL_OWNERSHIP_TRANSFER',
  'ENDORSEMENT_COC',
  'ENDORSEMENT_GOC',
];

/** Company name, or applicant name for licence types that have no company. */
export function applicantOrCompanyName(app: LicenseApplication): string | undefined {
  if (!app.licenseType?.key || !APPLICANT_NAME_TYPE_KEYS.includes(app.licenseType.key)) {
    return app.companyName ?? undefined;
  }
  const applicantName = (app.formData?.account as Record<string, unknown> | undefined)
    ?.applicantName;
  return typeof applicantName === 'string' && applicantName ? applicantName : undefined;
}

/** Reads a bilingual value for the active language, falling back to English. */
export function localized(value: Bilingual | undefined, language = 'en'): string {
  if (!value) return '';
  return (language === 'am' ? value.am : value.en) ?? value.en ?? value.am ?? '';
}

/**
 * Pulls the per-field issue list out of a failed submit so the wizard can say
 * exactly what is missing instead of a generic error.
 */
export function extractValidationIssues(error: unknown): ValidationIssue[] {
  const data = (error as { data?: Record<string, unknown> })?.data;
  if (!data) return [];
  const issues = data['issues'];
  return Array.isArray(issues) ? (issues as ValidationIssue[]) : [];
}

/**
 * Server error keys rendered for a human.
 *
 * The API answers with stable machine keys; showing those raw (e.g.
 * "capital_verification_required") tells an officer nothing about what to do.
 */
const ERROR_MESSAGES: Record<string, string> = {
  capital_verification_required:
    'Record the verified capital before approving — it must be checked against the bank letter.',
  capital_below_threshold:
    'The verified capital is below the minimum required for this licence type.',
  inspection_required_before_approval:
    'A physical inspection must be recorded before this application can be approved.',
  application_already_claimed: 'Another officer has already claimed this application.',
  assigned_to_another_officer:
    'This application is assigned to another officer, so only they can act on it.',
  not_application_owner: 'This application belongs to another applicant.',
  application_incomplete: 'The application is missing required information.',
  unresolved_remarks: 'Every requested correction must be addressed first.',
  adjustment_items_required: 'Flag at least one item before requesting an adjustment.',
  remark_text_required: 'Each flagged item needs a remark explaining what to fix.',
  rejection_reason_required: 'A rejection reason is required.',
  section_not_flagged_for_adjustment:
    'Only the sections the officer flagged can be changed in this round.',
  application_not_editable: 'This application can no longer be edited.',
  invalid_transition: 'That action is not available at this stage.',
  inspection_not_required_use_final_approve:
    'This licence type needs no inspection — approve it directly.',
  application_not_awaiting_inspection:
    'This application is not waiting for an inspection.',
  inspection_already_completed: 'This inspection has already been recorded.',
  license_type_inactive: 'This licence type is not currently accepting applications.',
};

export function extractErrorMessage(error: unknown, fallback = 'Something went wrong'): string {
  const data = (error as { data?: Record<string, unknown> })?.data;
  const raw = data?.['message'];

  // The filter passes structured errors through as `{ message: { message } }`.
  const key =
    typeof raw === 'string'
      ? raw
      : typeof (raw as { message?: unknown })?.message === 'string'
        ? ((raw as { message: string }).message)
        : undefined;

  if (key && ERROR_MESSAGES[key]) return ERROR_MESSAGES[key];
  if (typeof raw === 'string') return raw;
  if (Array.isArray(raw)) return raw.join(', ');
  return fallback;
}

/** One wizard step: either a group of form sections, or a fixed stage. */
export interface WizardStep {
  key: string;
  label: string;
  kind: 'sections' | 'staff' | 'documents' | 'review';
  sections: FormSectionConfig[];
}

/**
 * Turns the configured sections into wizard steps.
 *
 * Sections that share a `group` collapse onto one step, which is what keeps
 * the stepper short. Anything ungrouped keeps a step of its own, so a licence
 * type that has not been grouped still behaves exactly as before.
 */
export function buildWizardSteps(
  sections: FormSectionConfig[],
  formData: Record<string, Record<string, unknown>>,
  options?: {
    /**
     * Whether this licence type has staff-role requirements at all. Types
     * without any (seafarer registration) skip the Staff step entirely
     * instead of showing an empty page.
     */
    hasStaff?: boolean;
  },
): WizardStep[] {
  const visible = [...sections]
    .filter((section) => conditionHolds(section.showWhen, formData))
    .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));

  const steps: WizardStep[] = [];
  const byGroup = new Map<string, WizardStep>();

  for (const section of visible) {
    const group = section.group?.trim();
    if (!group) {
      steps.push({
        key: `section:${section.key}`,
        label: localized(section.title),
        kind: 'sections',
        sections: [section],
      });
      continue;
    }
    const existing = byGroup.get(group);
    if (existing) {
      existing.sections.push(section);
      continue;
    }
    const step: WizardStep = {
      key: `group:${group}`,
      label: group,
      kind: 'sections',
      sections: [section],
    };
    byGroup.set(group, step);
    steps.push(step);
  }

  // Honour explicit group ordering where given, keeping config order otherwise.
  const orderOf = (step: WizardStep) =>
    Math.min(...step.sections.map((s) => s.groupOrder ?? s.sortOrder ?? 0));
  steps.sort((a, b) => orderOf(a) - orderOf(b));

  // "Review" is a reserved group: those sections render on the final step
  // beside the summary rather than earning a step of their own. It is what
  // keeps a one-checkbox declaration from costing a whole page.
  const isReviewGroup = (step: WizardStep) =>
    step.sections.some((s) => s.group?.trim().toLowerCase() === 'review');
  const reviewSections = steps.filter(isReviewGroup).flatMap((s) => s.sections);
  const formSteps = steps.filter((step) => !isReviewGroup(step));

  return [
    ...formSteps,
    ...(options?.hasStaff === false
      ? []
      : [{ key: 'staff', label: 'Staff', kind: 'staff', sections: [] } as WizardStep]),
    { key: 'documents', label: 'Documents', kind: 'documents', sections: [] },
    { key: 'review', label: 'Review', kind: 'review', sections: reviewSections },
  ];
}

/** `${sectionKey}.${fieldKey}` → message, for inline display under a field. */
export type FieldErrors = Record<string, string>;

/**
 * Checks the required fields of the given sections against the answers,
 * skipping anything a condition currently hides. Used to stop the wizard
 * advancing past an incomplete step rather than failing only at submit.
 */
export function validateSections(
  sections: FormSectionConfig[],
  formData: Record<string, Record<string, unknown>>,
): FieldErrors {
  const errors: FieldErrors = {};

  for (const section of sections) {
    if (!conditionHolds(section.showWhen, formData)) continue;
    const values = formData[section.key] ?? {};

    for (const field of section.fields ?? []) {
      if (!conditionHolds(field.showWhen, formData)) continue;
      // Read-only values are supplied by the server, not the applicant.
      if (field.readOnly && field.source) continue;

      const value = values[field.key];
      const empty =
        value === undefined ||
        value === null ||
        (typeof value === 'string' && value.trim() === '') ||
        (typeof value === 'boolean' && value === false) ||
        (Array.isArray(value) && value.length === 0);

      if (field.required && empty) {
        errors[`${section.key}.${field.key}`] =
          field.type === 'BOOLEAN'
            ? `${localized(field.label)} must be accepted`
            : `${localized(field.label)} is required`;
        continue;
      }
      if (empty) continue;

      const numeric = Number(value);
      if (!Number.isNaN(numeric)) {
        if (field.min !== undefined && numeric < field.min) {
          errors[`${section.key}.${field.key}`] =
            `Must be at least ${field.min.toLocaleString()}`;
        } else if (field.max !== undefined && numeric > field.max) {
          errors[`${section.key}.${field.key}`] =
            `Must not exceed ${field.max.toLocaleString()}`;
        }
      }
    }
  }
  return errors;
}

/** Evaluates a config condition against the current form answers. */
export function conditionHolds(
  condition:
    | { field: string; equals?: unknown; notEquals?: unknown; in?: (string | number)[]; isSet?: boolean }
    | undefined
    | null,
  formData: Record<string, Record<string, unknown>>,
): boolean {
  if (!condition?.field) return true;
  const value = condition.field
    .split('.')
    .reduce<unknown>(
      (acc, key) =>
        acc && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined,
      formData as unknown,
    );
  const isEmpty = value === undefined || value === null || value === '';
  if (condition.isSet !== undefined) return condition.isSet ? !isEmpty : isEmpty;
  if (condition.equals !== undefined) return value === condition.equals;
  if (condition.notEquals !== undefined) return value !== condition.notEquals;
  if (condition.in !== undefined) return condition.in.includes(value as string | number);
  return true;
}
