import utils from "@/plugins/utils";
import dayjs from "dayjs";

export type AIInferenceResult = {
  aiGenerated: boolean;
  aiProbability: number;
  humanProbability: number;
};

export const C2PASourceTypeScreenCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture";
export const C2PASourceTypeDigitalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture";
export const C2PASourceTypeComputationalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture";
export const C2PASourceTypeCompositeCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture";
export const C2PASourceTypeTrainedAlgorithmicMedia =
  "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia";
export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia =
  "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia";

export type C2PAActionsAssertion = {
  actions: {
    action: string;
    softwareAgent?: string;
    digitalSourceType?: string;
  }[];
};

export type C2PAAssertion = {
  label: string;
  data: C2PAActionsAssertion | undefined;
};

export type C2PAManifest = {
  assertions: C2PAAssertion[];
  signature_info: {
    time: string;
  };
};

export type C2PAValidationResults = {
  activeManifest?: {
    failure: any[];
    success: any[];
    informational: any[];
  };
};

export type C2PAManifestInfo = {
  active_manifest: string;
  manifests: { [key: string]: C2PAManifest };
  validation_results?: C2PAValidationResults;
};

export type C2PAData = {
  manifest_info: C2PAManifestInfo;
};

export type Proof = {
  data?: any;
  name?: string;
  json?: string;
  integrity?: { pgp?: any; c2pa?: C2PAData; exif?: { [key: string]: string | Object }; opentimestamps?: any };
  ai?: { inferenceResult?: AIInferenceResult };
};

export type MediaMetadataGenerator =  "unknown" | "camera" | "screenshot" | "ai";
export type MediaMetadataPropertySource = "c2pa" | "exif" | "metadata";

export type MediaMetadataEdit = {
  editor: string;
  date?: Date;
}

export type MediaInterventionFlags = {
  creationDate?: Date;
  generator?: MediaMetadataGenerator;
  modified?: boolean;
  containsC2PA?: boolean;
  containsEXIF?: boolean;
}

export type MediaMetadata = {
  device?: string;
  creationDate?: Date;
  creationDateSource?: MediaMetadataPropertySource;
  generator?: MediaMetadataGenerator;
  generatorSource?: MediaMetadataPropertySource;
  edits?: MediaMetadataEdit[];
  containsC2PA?: boolean;
  containsEXIF?: boolean;
};

type FlagMatchRule = {
  field: string;
  match: string[];
  description: string;
};

type FlagMatchRulePathSegment = {
  object: any;
  path: string;
}

type FlagMatchRuleValue = {
  path: FlagMatchRulePathSegment[];
  value: string | object;
};

type FlagMatchInfo = {
  field: string;
  value: string;
  re: string;
};

type FlagValue = {
  path: string;
  transform?: (value: any, match: FlagMatchRuleValue) => any;
}

const pathsC2PASource = (): FlagValue[] => {
  return [
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Make", transform: getExifMakeModelPrefixed },
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Model", transform: getExifMakeModelPrefixed },
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/softwareAgent/name"},
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/softwareAgent"},
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../../claim_generator"}
  ];
};

const pathsExifSource = (): FlagValue[] => {
  return [
      {path: "integrity/exif/Make", transform: getExifMakeModelNoPrefix },
      {path: "integrity/exif/Model", transform: getExifMakeModelNoPrefix }
  ];
};

const pathsC2PACreationDate = (): FlagValue[] => {
  return [
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/when"},
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../metadata/dateTime"},
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../../signature_info/time"},
      {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:DateTimeOriginal", transform: (value, match) => {
        // Check if we have a timestamp offset
        let offset = extractFlagValues("../exif:OffsetTimeOriginal", match.path)?.at(0)?.value;
        return dayjs(Date.parse(value + (offset ? offset : ""))).format("lll");
      }}
  ];
}

const pathsC2PACamera = (): FlagValue[] => {
  return [
    {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:LensMake"},
    {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:LensModel"},
  ];
};

const pathsExifCamera = (): FlagValue[] => {
  return [
    {path: "integrity/exif/LensMake"},
    {path: "integrity/exif/LensModel"},
  ];
};

const getExifValue = (val: string): string => {
  return val.replace(/^"(.+(?="$))"$/, "$1");
};

const pathsExifCreationDate = (): FlagValue[] => {
  return [
      {path: "integrity/exif/DateTimeOriginal", transform: (value, match) => {
        let dateTimeOriginal = getExifValue(value);
        // Check if we have a timestamp offset
        let offset = extractFlagValues("../OffsetTimeOriginal", match.path)?.at(0)?.value as string;
        return dayjs(Date.parse(dateTimeOriginal + (offset ? getExifValue(offset) : ""))).format("lll");
      }}
  ];
}

const getExifMakeModelNoPrefix = (ignoredvalue: any, match: FlagMatchRuleValue): string => {
  return getExifMakeModel(match, "");
}

const getExifMakeModelPrefixed = (ignoredvalue: any, match: FlagMatchRuleValue): string => {
  return getExifMakeModel(match, "exif:");
}

const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string => {
      // Make and model
    let makeAndModel = "";
    const make = extractFlagValues(`../${prefix}Make`, match.path)?.at(0)?.value as string;
    const model = extractFlagValues(`../${prefix}Model`, match.path)?.at(0)?.value as string;
    if (make) {
      makeAndModel += getExifValue(make);
    }
    if (model) {
      if (makeAndModel.length > 0) {
        makeAndModel += ", ";
      }
      makeAndModel += getExifValue(model);
    }
    return makeAndModel;
}

const ruleScreenshotMeta = (): FlagMatchRule[] => {
  return [
    {
      field:
        "name",
      match: ["screenshot"],
      description: "Screen capture",
    },
  ];
};

const ruleAiMeta = (): FlagMatchRule[] => {
  const knownAIServices = [
    "ChatGPT",
    "OpenAI-API",
    "Adobe Firefly",
    "RunwayML",
    "Runway AI",
    "Google AI",
    "Stable Diffusion",
  ];
  return [
    { field: "name", match: ["^DALL_E_", "^Gen-3"], description: "File name" },
    {
      field: "integrity/c2pa/manifest_info/manifests[]/claim_generator",
      match: knownAIServices,
      description: "C2PA claim generator",
    },
    { field: "iptc/Credit", match: knownAIServices, description: "IPTC Credit" },
    { field: "iptc/Provider", match: knownAIServices, description: "IPTC Provider" },
    { field: "iptc/ImageSupplier[]", match: knownAIServices, description: "IPTC ImageSupplier" },
    { field: "iptc/ImageCreator[]", match: knownAIServices, description: "IPTC ImageCreator" },
  ];
};

const matchFlag = (rules: FlagMatchRule[], path: FlagMatchRulePathSegment[]) => {
  let result = false;
  let resultInfo: FlagMatchInfo[] = [];
  for (let rule of rules) {
    try {
      const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi"));
      const values = extractFlagValues(rule.field, path);
      values.forEach((v) => {
        re.forEach((r) => {
          if (typeof v.value === "string" && r.test(v.value)) {
            result = true;
            resultInfo.push({ field: v.path.map((v) => v.path).join("/"), value: v.value, re: r.source });
          }
        });
      });
    } catch (e) {
      console.error("Invalid RE", e);
    }
  }
  return { result: result, matches: resultInfo };
};

const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => {
  const getValues = (
    keys: string[],
    path: FlagMatchRulePathSegment[]
  ): FlagMatchRuleValue[] | undefined => {
    if (keys.length == 0 || path.length == 0) return undefined;
    
    const o = path[0].object;
    let key = keys[0];

    const lastBracket = key.lastIndexOf("[");
    if (key === "..") {
      return getValues(keys.slice(1), path.slice(1));
    }
    if (key.endsWith("]") && lastBracket > 0) {
      const optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
      key = key.substring(0, lastBracket);
      if (o[key] != undefined) {
        let opart: any[] = o[key];
        if (!Array.isArray(opart)) {
          opart = Object.values(opart) ?? [];
        }

        // Any constraints controlling what array object(s) to consider?
        if (optionalConstraint) {
          if (optionalConstraint.startsWith("!!")) {
            // Ignore this path in the tree if ANY of the values contain the constraint match.
            const [prop, val] = optionalConstraint.substring(2).split("=");
            if (opart.some((item) => item[prop] === val)) {
              return undefined;
            }
          } else if (optionalConstraint.startsWith("!")) {
            const [prop, val] = optionalConstraint.substring(1).split("=");
            opart = opart.filter((item) => item[prop] !== val);
          } else {
            const [prop, val] = optionalConstraint.split("=");
            let valarray = val.split("|");
            opart = opart.filter((item) => valarray.includes(item[prop]));
          }
        }

        if (keys.length == 1) {
          return opart.map((s, i) => {
            return { value: o, path: [{ object: s, path: key + "[" + i + "]"}, ... path] };
          });
        } else {
          return opart.reduce((res: FlagMatchRuleValue[] | undefined, oin: any, i: number) => {
            let matches = getValues(keys.slice(1), [ { object: oin, path: key + "[" + i + "]"}, ... path]);
            if (matches) {
              const r2 = res || [];
              r2.push(...matches);
              return r2;
            }
            return res;
          }, undefined);
        }
      } else {
        return undefined;
      }
    } else {
      if (o[key] != undefined) {
        if (keys.length == 1) {
          return [{ value: o[key], path: [{object: o[key], path: key}, ...path] }];
        } else {
          return getValues(keys.slice(1), [{object: o[key], path: key}, ...path]);
        }
      } else {
        return undefined;
      }
    }
  };

  let result: FlagMatchRuleValue[] = [];
  try {
    let keys = flagPath.split("/");
    result = getValues(keys, path) ?? [];
  } catch (e) {
    console.error("Invalid RE", e);
  }
  return result;
};

const getFirstWithData = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): string | undefined => {
  let first: FlagMatchRuleValue | undefined = undefined;
  let transform: ((value: any, match: FlagMatchRuleValue) => any) | undefined = undefined;
  for (let idx = 0; idx < flagValues.length; idx++) {
    const result = extractFlagValues(flagValues[idx].path, path);
    if (result.length > 0) {
      first = result[0]
      transform = flagValues[idx].transform;
      break;
    }
  }
  if (first && first.value as string) {
    try {
      let val = first.value as string;
      if (val && transform) {
        val = transform(val, first);
      }
      return val;
    } catch (error) {
    }
  }
  return undefined;
};

const getFirstWithDataAsDate = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): Date | undefined => {
  let val = getFirstWithData(flagValues, path);
  if (val) {
    try {
      let date = new Date(Date.parse(val));
      if (isNaN(date.valueOf())) {
        // Try EXIF format
        date = utils.parseExifDate(val);
      }
      return date;
    } catch (error) {
    }
  }
  return undefined;
};

export const mediaMetadataToMediaInterventionFlags = (mediaMetadata: MediaMetadata): MediaInterventionFlags => {
  return {
    creationDate: mediaMetadata.creationDate,
    generator: mediaMetadata.generator,
    modified: mediaMetadata.edits && mediaMetadata.edits.length > 0,
    containsC2PA: mediaMetadata.containsC2PA,
    containsEXIF: mediaMetadata.containsEXIF
  }
}

export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined => {
  if (!proof) return undefined;

  let edits: MediaMetadataEdit[] | undefined = undefined;
  let valid = false;

  try {
    let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest;
    if (results) {
      valid = results.failure.length == 0 && results.success.length > 0;
    }

    const rootMatchPath = [{object: proof, path: ""}];

    let source: string | undefined = undefined;
    let dateCreated: Date | undefined = undefined;
    let dateCreatedSource: MediaMetadataPropertySource | undefined = undefined;

    source = getFirstWithData(pathsC2PASource(), rootMatchPath);
    if (!source) {
      source = getFirstWithData(pathsExifSource(), rootMatchPath);
    }

    dateCreated = getFirstWithDataAsDate(pathsC2PACreationDate(), rootMatchPath);
    if (dateCreated) {
      dateCreatedSource = "c2pa";
    } else {
      dateCreated = getFirstWithDataAsDate(pathsExifCreationDate(), rootMatchPath);
      if (dateCreated) {
        dateCreatedSource = "exif";
      }
    }


    let generator: MediaMetadataGenerator = "unknown";
    let generatorSource: MediaMetadataPropertySource | undefined = undefined;

    let digitalSourceType = extractFlagValues("integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/digitalSourceType", rootMatchPath)?.at(0)?.value as string;
    if ([C2PASourceTypeScreenCapture].includes(digitalSourceType)) {
      generator = "screenshot";
      generatorSource = "c2pa";
    } else if ([C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture].includes(digitalSourceType)) {
      generator = "camera";
      generatorSource = "c2pa";
    } else if ([C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia].includes(digitalSourceType)) {
      generator = "ai";
      generatorSource = "c2pa";
    } else if (getFirstWithData(pathsC2PACamera(), rootMatchPath)) {
      generator = "camera";
      generatorSource = "c2pa";
    } else if (getFirstWithData(pathsExifCamera(), rootMatchPath)) {
      generator = "camera";
      generatorSource = "exif";
    } else if (matchFlag(ruleScreenshotMeta(), rootMatchPath).result) {
      generator = "screenshot";
      generatorSource = "metadata";
    } else if (matchFlag(ruleAiMeta(), rootMatchPath).result) {
      generator = "ai";
      generatorSource = "metadata";
    }

    console.error("PROOF", proof);

    const c2paEdits = extractFlagValues(
      "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[!!action=c2pa.created]",
      rootMatchPath
    );
    if (c2paEdits.length > 0) {
      edits = c2paEdits.map((edit) => {
        return {
          editor: getFirstWithData([
              {path: "softwareAgent/name"}, 
              {path: "softwareAgent"}, 
              {path: "../../../claim_generator"}
            ], edit.path) ?? "",
          date: getFirstWithDataAsDate([
              {path: "when"}, 
              {path: "../../metadata/dateTime"}, 
              {path: "../../../signature_info/time"}
            ], edit.path)
        }
      });
    }

    // Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults.
    if (source === undefined && dateCreated === undefined && generator === "unknown" && (!edits || edits.length == 0)) {
      return undefined;
    }

    const flags: MediaMetadata = {
      device: source,
      creationDate: dateCreated,
      creationDateSource: dateCreatedSource,
      generator: generator,
      generatorSource: generatorSource,
      edits: edits,
      containsC2PA: proof.integrity?.c2pa !== undefined,
      containsEXIF: proof.integrity?.exif !== undefined
    };
    return flags;
  } catch (error) {}
  return undefined;
};
