const util = require('util');
const exec = util.promisify(require('child_process').exec);
const path = require('path');
const { promises: fs } = require("fs");
const axios = require('axios');

const ROOT_PATH = '/app';
const MAX_BUFFER = 1024 * 1024 * 50;
const GITEA_DOMAIN = 'gitea.flatlogic.app';
const USERNAME = 'admin';
const API_TOKEN = 'f22e83489657f49c320d081fc934e5c9daacfa08';

class VCS {
    // Main method – controller of the repository initialization process
    static async initRepo(projectId = 'test') {
        try {
            // 1. Ensure the remote repository exists (create if needed)
            const remoteUrl = await this.setupRemote(projectId);

            // 2. Set up the local repository (initialization, fetching and reset)
            await this.setupLocalRepo(remoteUrl);

            console.log(`[DEBUG] Repository "${projectId}" is ready (remote code applied).`);
            return { message: `Repository ${projectId} is ready (remote code applied).` };
        } catch (error) {
            throw new Error(`Error during repo initialization: ${error.message}`);
        }
    }

    // Checks for the existence of the remote repo and creates it if it doesn't exist
    static async setupRemote(projectId) {
        console.log(`[DEBUG] Checking remote repository "${projectId}"...`);
        let repoData = await this.checkRepoExists(projectId);
        if (!repoData) {
            console.log(`[DEBUG] Remote repository "${projectId}" does not exist. Creating...`);
            repoData = await this.createRemoteRepo(projectId);
            console.log(`[DEBUG] Remote repository created: ${JSON.stringify(repoData)}`);
        } else {
            console.log(`[DEBUG] Remote repository "${projectId}" already exists.`);
        }
        // Return the URL with token authentication
        return `https://${USERNAME}:${API_TOKEN}@${GITEA_DOMAIN}/${USERNAME}/${projectId}.git`;
    }

    // Sets up the local repository: either fetches/reset if .git exists,
    // initializes git in a non-empty directory, or clones the repository if empty.
    static async setupLocalRepo(remoteUrl) {
        const gitDir = path.join(ROOT_PATH, '.git');
        const localRepoExists = await this.exists(gitDir);
        if (localRepoExists) {
            await this.fetchAndResetRepo();
        } else {
            const files = await fs.readdir(ROOT_PATH);
            if (files.length > 0) {
                await this.initializeGitRepo(remoteUrl);
            } else {
                console.log('[DEBUG] Local directory is empty. Cloning remote repository...');
                await exec(`git clone ${remoteUrl} .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            }
        }
    }

    // Check if a file/directory exists
    static async exists(pathToCheck) {
        try {
            await fs.access(pathToCheck);
            return true;
        } catch {
            return false;
        }
    }

    // If the local repository exists, fetches remote data and resets the repository state
    static async fetchAndResetRepo() {
        console.log('[DEBUG] Local repository exists. Fetching remote...');
        await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        const branchReset = await this.tryResetToBranch('fl-dev');

        if (!branchReset) {
            // If 'fl-dev' branch is not found, try 'master'
            const masterReset = await this.tryResetToBranch('master');
            if (masterReset) {
                // Create 'fl-dev' branch and push it to remote
                console.log('[DEBUG] Creating and switching to branch "fl-dev"...');
                await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
                await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
                console.log('[DEBUG] Pushing fl-dev branch to remote...');
                await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            } else {
                // If neither remote master nor fl-dev exist – make an initial commit
                console.log('[DEBUG] Neither "origin/fl-dev" nor "origin/master" exist. Creating initial commit...');
                await this.commitInitialChanges();
            }
        }
    }

    // Tries to check out and reset to the specified branch
    static async tryResetToBranch(branchName) {
        try {
            console.log(`[DEBUG] Checking for remote branch "origin/${branchName}"...`);
            await exec(`git rev-parse --verify origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            console.log(`[DEBUG] Remote branch "origin/${branchName}" found. Resetting local repository to "origin/${branchName}"...`);
            await exec(`git reset --hard origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            await exec(`git checkout ${branchName === 'fl-dev' ? 'fl-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            return true;
        } catch (e) {
            console.log(`[DEBUG] Remote branch "origin/${branchName}" does NOT exist.`);
            return false;
        }
    }

    // If remote branch doesn't exist, make the initial commit and set up branches
    static async commitInitialChanges() {
        console.log('[DEBUG] Adding all files for initial commit...');
        await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        const { stdout: status } = await exec(`git status --porcelain`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        if (status.trim()) {
            await exec(`git commit -m "Initial version"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            await exec(`git push -u origin master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            console.log('[DEBUG] Creating and switching to branch "fl-dev"...');
            await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            console.log('[DEBUG] Making fl-dev branch identical to master...');
            await exec(`git reset --hard origin/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            console.log('[DEBUG] Pushing fl-dev branch to remote...');
            await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        } else {
            console.log('[DEBUG] No local changes to commit.');
        }
    }

    // If the local directory is not empty but .git doesn't exist, initialize git,
    // add .gitignore, configure the user, and add the remote origin.
    static async initializeGitRepo(remoteUrl) {
        console.log('[DEBUG] Local directory is not empty. Initializing git...');
        const gitignorePath = path.join(ROOT_PATH, '.gitignore');
        const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`;
        await fs.writeFile(gitignorePath, ignoreContent, 'utf8');

        await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        console.log('[DEBUG] Configuring git user...');
        await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

        console.log(`[DEBUG] Adding remote ${remoteUrl}...`);
        await exec(`git remote add origin ${remoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

        console.log('[DEBUG] Fetching remote...');
        await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        try {
            console.log('[DEBUG] Checking for remote branch "origin/fl-dev"...');
            await exec(`git rev-parse --verify origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            console.log('[DEBUG] Remote branch "origin/fl-dev" exists. Resetting local repository to origin/fl-dev...');
            await exec(`git reset --hard origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            console.log('[DEBUG] Switching to branch "fl-dev"...');
            await exec(`git checkout -B fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
        } catch (e) {
            console.log('[DEBUG] Remote branch "origin/fl-dev" does NOT exist. Creating initial commit...');
            await this.commitInitialChanges();
        }
    }

    // Method to check if the repository exists on remote server
    static async checkRepoExists(repoName) {
        const url = `https://${GITEA_DOMAIN}/api/v1/repos/${USERNAME}/${repoName}`;
        try {
            const response = await axios.get(url, {
                headers: { Authorization: `token ${API_TOKEN}` }
            });
            return response.data;
        } catch (err) {
            if (err.response && err.response.status === 404) {
                return null;
            }
            throw new Error('Error checking repository existence: ' + err.message);
        }
    }

    // Method to create a remote repository via API
    static async createRemoteRepo(repoName) {
        const createUrl = `https://${GITEA_DOMAIN}/api/v1/user/repos`;
        console.log("[DEBUG] createUrl", createUrl);
        try {
            const response = await axios.post(createUrl, {
                name: repoName,
                description: `Repository for project ${repoName}`,
                private: false
            }, {
                headers: { Authorization: `token ${API_TOKEN}` }
            });
            return response.data;
        } catch (err) {
            throw new Error('Error creating repository via API: ' + err.message);
        }
    }

    static async commitChanges(message = "", files = '.') {
        // Ensure that we are on branch 'fl-dev' before making any commits
        await this._ensureDevBranch();
        try {
            await exec(`git add ${files}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            const { stdout: status } = await exec('git status --porcelain', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            if (!status.trim()) {
                return { message: "No changes to commit" };
            }
            const now = new Date();
            const commitMessage = message || `Auto commit: ${now.toISOString()}`;
            console.log('commitMessage:', commitMessage);
            await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            await this._pushChanges();
            console.log('Pushed');
            return { message: "Changes committed" };
        } catch (error) {
            throw new Error(`Error during commit: ${error.message}`);
        }
    }

    static async getLog() {
        try {
            const { stdout } = await exec('git log fl-dev --oneline', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== '');
            const result = {};
            lines.forEach((line) => {
                const firstSpaceIndex = line.indexOf(' ');
                if (firstSpaceIndex > 0) {
                    const hash = line.substring(0, firstSpaceIndex);
                    const message = line.substring(firstSpaceIndex + 1).trim();
                    result[hash] = message;
                }
            });
            return result;
        } catch (error) {
            throw new Error(`Error during get log: ${error.message}`);
        }
    }

    static async checkout(ref) {
        try {
            await exec(`git checkout ${ref}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            return { message: `Checked out to ${ref}` };
        } catch (error) {
            throw new Error(`Error during checkout: ${error.message}`);
        }
    }

    static async revert(commitHash) {
        try {
            await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

            // Rollback to the specified commit hash
            await exec(
                `git revert --no-edit ${commitHash}..HEAD --no-commit`,
                { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
            );
            // Commit the changes
            await exec(
                `git commit -m "Revert to version ${commitHash}"`,
                { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
            );
            await this._pushChanges();

            return { message: `Reverted to commit ${commitHash}` };
        } catch (error) {
            console.error("Error during range revert:", error.message);
            if (error.stdout) {
                console.error("Revert stdout:", error.stdout);
            }
            if (error.stderr) {
                console.error("Revert stderr:", error.stderr);
            }
            throw new Error(`Error during range revert: ${error.message}`);
        }
    }

    static async mergeDevIntoMaster() {
        try {
            // Switch to branch 'master'
            console.log('Switching to branch "master"...');
            await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

            // Merge branch 'fl-dev' into 'master' with a forced merge.
            // Parameter -X theirs is used to resolve conflicts by keeping the changes from the branch being merged in case of conflicts.
            console.log('Merging branch "fl-dev" into "master" (force merge with -X theirs)...');
            await exec(
                `git merge fl-dev --no-ff -X theirs -m "Forced merge: merge fl-dev into master"`,
                { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
            );

            // Push the merged 'master' branch to remote
            console.log('Pushing merged master branch to remote...');
            const { stdout, stderr } = await exec(`git push origin master`, {
                cwd: ROOT_PATH,
                maxBuffer: MAX_BUFFER
            });
            if (stdout) {
                console.log("Git push stdout:", stdout);
            }
            if (stderr) {
                console.error("Git push stderr:", stderr);
            }
            return { message: "Branch fl-dev merged into master and pushed to remote" };
        } catch (error) {
            console.error("Error during mergeDevIntoMaster:", error.message);
            if (error.stdout) {
                console.error("Merge stdout:", error.stdout);
            }
            if (error.stderr) {
                console.error("Merge stderr:", error.stderr);
            }
            throw error;
        }
    }

    static async resetDevBranch() {
        try {
            console.log('[DEBUG] Switching to branch "master"...');
            await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

            console.log('[DEBUG] Resetting branch "fl-dev" to be identical to "master"...');
            // Command checkout -B fl-dev master creates branch 'fl-dev' from 'master' and switches to it
            await exec(`git checkout -B fl-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

            console.log('[DEBUG] Pushing updated branch "fl-dev" to remote (force push)...');
            await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });

            return { message: 'fl-dev branch successfully reset to master.' };
        } catch (error) {
            console.error("Error during resetting fl-dev branch:", error.message);
            if (error.stdout) {
                console.error("Reset stdout:", error.stdout);
            }
            if (error.stderr) {
                console.error("Reset stderr:", error.stderr);
            }
            throw new Error(`Error during resetting fl-dev branch: ${error.message}`);
        }
    }

    static async _pushChanges() {
        try {
            const { stdout, stderr } = await exec(`git push origin fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            if (stdout) {
                console.log("Git push stdout:", stdout);
            }
            if (stderr) {
                console.error("Git push stderr:", stderr);
            }
            return { message: "Changes pushed to remote repository (fl-dev branch)" };
        } catch (error) {
            console.error("Git push error:", error.message);
            if (error.stdout) {
                console.error("Git push stdout:", error.stdout);
            }
            if (error.stderr) {
                console.error("Git push stderr:", error.stderr);
            }
        }
    }

    static async _ensureDevBranch() {
        try {
            // Check if branch 'fl-dev' exists
            const { stdout: branchList } = await exec(`git branch --list fl-dev`, {
                cwd: ROOT_PATH,
                maxBuffer: MAX_BUFFER,
            });

            if (!branchList || branchList.trim() === '') {
                console.log("Branch 'fl-dev' not found. Creating branch 'fl-dev'.");
                await exec(`git checkout -b fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
            } else {
                // Determine current branch
                const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, {
                    cwd: ROOT_PATH,
                    maxBuffer: MAX_BUFFER,
                });
                const currentBranch = currentBranchStdout.trim();
                if (currentBranch !== 'fl-dev') {
                    console.log(`Switching from branch '${currentBranch}' to 'fl-dev'.`);
                    await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
                } else {
                    console.log("Already on branch 'fl-dev'.");
                }
            }
        } catch (error) {
            console.error("Error ensuring branch 'fl-dev':", error.message);
            throw error;
        }
    }
}

module.exports = VCS;