// Make sure you have created a '.env' field in the top-level of
// this NodeJS project, and set the following lines accordingly
// with the relevant values from your OpenAI account:
//
//   OPENAI_API_KEY=sk-????????????????????????????????????????????????
//   ORGANIZATION_ID=org-????????????????????????


// For an (alternative) approach that looks like it retrieve the already created Assistant directly, see:
//    https://gist.github.com/drifterz13/0cbe93ced5dc7958d7841a29c1721d1c

import {
    //Configuration,
    OpenAI,
    //ChatCompletionRequestMessageRoleEnum
} from "openai";


import {
    FileCitation,
    MessageCreateParams,
    TextContentBlock
} from 'openai/resources/beta/threads/messages'

import type { NextApiRequest, NextApiResponse } from "next";

const openai = new OpenAI({
  apiKey:       process.env.OPENAI_API_KEY,
  organization: process.env.ORGANIZATION_ID
});

let GS3AssistantLookup : any = null

async function initGS3AssistantsLookup() {
    GS3AssistantLookup = new Map<string, any>();

    console.log("Setting up GS3AssistantsLookup table");

    console.log("  Retriving first page block, reverse order, so latest GS3 assistant IDs stored");
    // AssistantsPage
    //let assistants_list_cursor : CursorPage<Assistant> = await openai.beta.assistants.list({
    let assistants_list_cursor = await openai.beta.assistants.list({
	order: "asc",
	limit: 20,
    });
    let paginated_items = assistants_list_cursor.getPaginatedItems();
    
    while (paginated_items.length > 0) {
	let last_checked_assistant_id = "";
	
	for (const openai_assistant of paginated_items) {
	    const current_assistant_id = openai_assistant.id;
	    
	    const metadata : any = openai_assistant.metadata;
	    
	    if ("greenstone3" in metadata) {
		if (metadata.greenstone3 === "true") {
		    console.log("    Processing Greenstone3 Assistant:");
		    //console.log(openai_assistant);
		    
		    const site_name = metadata.siteName;
		    const coll_name = metadata.collectionName;

		    //console.log(`    Adding entry for site: ${site_name}, collect: ${coll_name}`);
		    
		    if (!(site_name in GS3AssistantLookup)) {
			GS3AssistantLookup[site_name] = new Map<string, any>();
		    }
		    else {
			// site already setup and going ....
			// check to see if pre-existign coll_name already there?
			if (coll_name in GS3AssistantLookup[site_name]) {
			    console.error(`Warning: Overwriting pre-existing GS3 assistant for site: ${site_name}, collection: ${coll_name}`);
			}
		    }
		    
		    console.log(`    Storing GS3AssistantsLook[${site_name}][${coll_name}]: ${current_assistant_id}`);
		    GS3AssistantLookup[site_name][coll_name] = current_assistant_id;

		}		
	    }
	    last_checked_assistant_id = current_assistant_id;
	}

	
	console.log("  Retrieving next page block, ordered by most recent assistants");
	assistants_list_cursor = await openai.beta.assistants.list({
	    after: last_checked_assistant_id,
	    order: "asc",
	    limit: 20,
	});
	paginated_items = assistants_list_cursor.getPaginatedItems();
    }
    console.log("Finished setting up GS3AssistantsLookup table");
}

async function retrieveOpenaiFileExcerpt(openai_file: FileCitation, start_index: number, end_index: number)
{
    const file_id: string = openai_file.id;
    const filename:string = openai_file.filename;
    
    //const openai_file = await openai.files.retrieve(file_id);


    //console.log("*** OpenAI file");
    //console.log(openai_file);
    console.log("*** file_id = " + file_id);
    
    const file_content = await openai.files.content(file_id);
    console.log("*** OpenAI file content");
    console.log(file_content);

    // https://community.openai.com/t/how-do-download-files-generated-in-ai-assistants/493516/2

    const excerpt = file_content.substring(start_index,end_index);
    
/*
    try {
	const data = fs.readFileSync('/Users/joe/test.txt', 'utf8');
	console.log(data);
    }
    catch (err) {
	console.error(err);
    }
*/
    
}

async function processAnnotations(message : TextContentBlock)
{
    console.log("Process Annotations");
    
    const messageContentText = message.text;
    const annotations    = messageContentText.annotations;

    const citations: string[] = [];

    var from_gsdocid_regex = new RegExp(/<span fromGSDocId="(.*?)">/, "g");
    
    // Iterate over the annotations and add footnotes
    const annotations_len = annotations.length;
    for (let i=0; i<annotations_len; i++) {
	const annotation = annotations[i];
	const index      = i;

	// Replace the text with a footnote
	console.log(`  Processing Annotation[${index}]: `, annotation, "--");

	messageContentText.value = messageContentText.value.replace(annotation.text, ` [${index+1}]`);
	
	// Gather citations based on annotation attributes
	if ("file_citation" in annotation) {
	    const file_citation = annotation.file_citation;
	    console.log("****** file_citation: ");
	    console.log(file_citation);
	    
	    console.log(`    Retrieving file_citation for ${file_citation.file_id}`);
	    const cited_file = await openai.files.retrieve(file_citation.file_id);
	    console.log("****!!! cited_file: ");
	    console.log(cited_file);

	    // https://community.openai.com/t/how-can-i-access-the-specific-text-of-the-file-that-the-annotation-is-referencing/726723/3
	    
	    const start_index = annotation.start_index;
	    const end_index   = annotation.end_index;
	    
	    //const quote_html = await retrieveOpenaiFileExcerpt(cited_file, start_index, end_index);
	    
	    // //const quote_html = file_citation.quote;
	    //console.log("*** need to check if file_citation.text is new equivalent to file_fitation.quote")
	    // const quote_html = file_citation.text;

	    //console.log("*** need to find out new equivalent to file_fitation.quote")
	    const quote_html = "from Fake Excerpt";
	    const quote_excerpt = `excerpt location [${start_index},${end_index}] from
`
	    let source_reference = cited_file.filename;

	    // Change:
	    //  thewillow|community-contributions|WCA_OH_M04.html
	    //
	    // Into:
	    // https://thewillow-aws.sowemustthink.space/greenstone3/library/collection/community-contributions/document/WCA_OH_M04?p.s=AdvancedFieldQuery
	    
	    const linked_source_reference = source_reference.replace(/(.*?)\|(.*?)\|(.*?)\.html?/g,"[$3](https://thewillow-aws.sowemustthink.space/greenstone3/library/collection/community-contributions/document/$3)");
	    
	    //const from_gsdocid_matches = from_gsdocid_regex.exec(quote_html);

	    const from_gsdocid_matches = quote_html.match(/<span fromGSDocId="(.*?)">/);
		
	    if ((from_gsdocid_matches !== null) && (from_gsdocid_matches.length > 0)) {
		console.log("**** Matches!!");
		console.log(from_gsdocid_matches);
		source_reference = from_gsdocid_matches[1];

		source_reference = `[${source_reference}](https://thewillow-aws.sowemustthink.space/greenstone3/library/collection/community-contributions/document/${source_reference})`;
		
	    }
		
	    let quote = quote_html.replace(/(<([^>]+)>)/g," ").replace(/(\\n|\n)/," ").replace(/\s+/g," ")
	    if (quote.length > 120) {
		quote=quote.replace(/^(.{118,160})\s+.*$/,"$1 ...");
	    }
	    console.log(`    quote = ${quote}`);
	    
	    //citations.push(`[${index+1}] ${quote} -- (from ${source_reference})`);
	    citations.push(`[${index+1}] ${quote_excerpt} ${linked_source_reference}`);
	    
	}
	else if ("file_path" in annotation) {
	    const file_path = annotation.file_path;
	    console.log(`    Retrieving file_path for ${annotation.file_path.file_id}`);
	    const cited_file = await openai.files.retrieve(annotation.file_path.file_id);
	    citations.push(`[${index}] Click <here> to download ${cited_file.filename}`);
	    // Note: File download functionality not implemented above for brevity
	}
    }

    if (annotations_len > 0) {
	// Add footnotes to the end of the message before displaying to the user
	console.log("Citations: ", citations, '\n');
    		
	messageContentText.value += '\n\n----\n## Sources\n' + citations.join('\n\n');
    }
    

}

    
const UserThreadIdLookup = new Map<string, any>();

async function assistantHandler(
    req: NextApiRequest,
    res: NextApiResponse
) {    
    if (GS3AssistantLookup === null) {
	await initGS3AssistantsLookup();
    }

    // ****
    const assistantIdToUse = GS3AssistantLookup["thewillow"]["community-contributions"];
    //console.log("*** Overriding assistantIdToUse = asst_QMq5sQA2DERl1QEpBLzVHBvo");	
    //const assistantIdToUse = "asst_QMq5sQA2DERl1QEpBLzVHBvo";

    console.log("*** Using assistantID: " + assistantIdToUse);
    
    //const userId = req.body.userId; // You should include the user ID in the request
    const userId = "test_username";

    
    // Create a new thread if it's the user's first message
    if (!UserThreadIdLookup.has(userId)) {
	try {
	    const myThread = await openai.beta.threads.create();
	    console.log("========")
	    console.log(`Created new thread with ID: '${myThread.id}'"`);
	    console.log("========")
	    
	    UserThreadIdLookup.set(userId,myThread.id); // Store the thread ID for this user
	}
	catch (error) {
	    console.error("Error creating thread:", error);
	    res.status(500).json({ error: "Internal server error" });
	    return;
	}
    }

    //console.log("*** req.body");
    //console.log(req.body);
    const num_messages = req.body.messages.length;
    
    const userMessage = req.body.messages[num_messages-1].content;    
    //const userMessage = "What were some of the names of the DJs who worked at The Willow?";

    console.log("--------");
    console.log("userMessage to be submitted: " + userMessage);
    console.log("--------");
    
    // Add a Message to the Thread
    try {
	console.log("  Retrieving thread for userId: " + userId);
	const myThreadId = UserThreadIdLookup.get(userId);
	console.log("  myThreadId = " + myThreadId);
	console.log("");

	const user_message = {
	    role: "user",
	    content: userMessage
	} as MessageCreateParams;

	const myThreadMessage = await openai.beta.threads.messages.create(myThreadId,user_message);

	// Run the Assistant
	console.log("Starting run for threadId: " + myThreadId);
	const myRun = await openai.beta.threads.runs.create(myThreadId, { assistant_id: assistantIdToUse });
	console.log("myRun: ", myRun, "\n");

	const checkStatusUntilCompleted = async (thread_id: string, run_id: string) => {
	    // Give a run that's just started some time to get going
	    await new Promise((resolve) => setTimeout(resolve, 5000));	    
	    let run = await openai.beta.threads.runs.retrieve(thread_id,run_id);

	    let run_loop_count = 0;
	    while ((run.status === "queued") || (run.status === "in_progress")) {
		run_loop_count++;
		
		await new Promise((resolve) => setTimeout(resolve, 1000));
		console.log(`  ${thread_id} run_id ${run_id}: Getting update on progress [ ` + "=".repeat(run_loop_count)+"> ]");
		run = await openai.beta.threads.runs.retrieve(thread_id,run_id);
		console.log(`  ${thread_id} run_id ${run_id}: Status = ${run.status}`);
		
		// The following is not strictly necessary, but nice to have logged
		if (run.status === "completed") {
		    console.log(`  ${thread_id} run_id ${run_id}: Completed!`);
		    console.log("--------");
		    break;
		}		
	    }
	}
	
	// Retrieve the Messages added by the Assistant to the Thread
	const waitForAssistantMessage = async () => {
	    console.log("waitForAssistantMessage() away to call await checkStatusUntilComplete()");
	    await checkStatusUntilCompleted(myThreadId,myRun.id);
	    console.log("waitForAssistantMessage() returned from checkRunUntilComplete()");
	    
	    console.log(`Retrieving messages list for ${myThreadId}`);
	    const allMessages = await openai.beta.threads.messages.list(myThreadId);
	    
	    console.log("--------\n");

	    const user_request_message: TextContentBlock = (myThreadMessage.content[0]) as TextContentBlock;
	    const assistant_response_message = (allMessages.data[0].content[0]) as TextContentBlock;

	    console.log("#####")
	    console.log("Assistant response message:", assistant_response_message, "\n");
	    console.log("#####")

	    await processAnnotations(assistant_response_message);
	    
	    console.log("User: ", user_request_message.text.value);
	    console.log("Assistant: ", assistant_response_message.text.value);

	    var returned_message = {
		role: "assistant",
		content: assistant_response_message.text.value
	    };
	    
	    return returned_message;
	};

	const assistant_message = waitForAssistantMessage();

	return assistant_message;
    }
    catch (error) {
	console.error("Error:", error);
	res.status(500).json({ error: "Internal server error" });
    }
}





async function chatHandler(
    req: NextApiRequest,
    res: NextApiResponse
) {    
    const assistant_message = await assistantHandler(req,res);

    console.log("++++++++");
    console.log("Assistant completion message: ");
    console.log(assistant_message);
    console.log("++++++++");
    
    res.status(200).json({ result: assistant_message });    
}

export default chatHandler;
