import request from 'superagent'
import fs from 'fs'

/**
 * JiraService provides methods to interact with the Jira REST API
 * This service handles authentication, issue management, comments, transitions, and more
 */
export default class JiraService {
    /**
     * Creates a new instance of the JiraService
     * @param {Object} config - Configuration object for the Jira service
     * @param {string} config.baseUrl - The base URL of the Jira instance
     * @param {string} config.username - The username for authentication
     * @param {string} config.password - The password for authentication
     * @param {string} config.projectKey - The Jira project key (e.g., PROJ)
     * @param {string} config.boardId - The Jira board ID for agile operations
     */
    constructor( config ) {
        this.request = request;
        this.config = config;
    }

    /**
     * Gets the current date in DD/MM/YY format
     * @returns {string} - Formatted date string
     */
    getDate() {
        const date = new Date();

        let dateStr = (date.getDate());
        dateStr += '/';
        dateStr += date.getMonth()+1;
        dateStr += '/';
        dateStr += date.getFullYear().toString().substring(2, 4);

        return dateStr;
    };

    /**
     * Gets the ID of the project configured in this service
     * @returns {Promise<string|null>} - The project ID or null if not found
     */
    async getProjectId() {
        try {
            let response = await this.request
            .get( this.config.baseUrl + '/rest/api/latest/project/' + this.config.projectKey )
            .auth(this.config.username, this.config.password);
            if (response.body.id) {
                return response.body.id;
            } else {
                console.error('Project ID not found.');
                return null;
            }
        } catch (error) {
            console.error('Unable to get jira project id, error: ' + error);
            return null;
        }
    };

    /**
     * Gets the ID of the currently active sprint for the configured board
     * @returns {Promise<string|null>} - The active sprint ID or null if not found
     */
    async getActiveSprintId() {
        try {
            let response = await this.request
                .get(this.config.baseUrl + '/rest/agile/latest/board/' + this.config.boardId + '/sprint')
                .query({ state: 'active' })
                .auth(this.config.username, this.config.password);
                
            if (response.body && response.body.values && response.body.values.length > 0) {
                return response.body.values[0].id;
            } else {
                console.error('Active sprint ID not found.');
                return null;
            }
        } catch (error) {
            console.error('Unable to get jira Active sprint id, error: ' + error);
            if (error.response) {
                console.error('Response status: ' + error.response.status);
                console.error('Response body: ' + JSON.stringify(error.response.body));
            }
            return null;
        }
    };

    /**
     * Gets all versions for the configured board
     * @returns {Promise<Array|null>} - Array of version objects or null if an error occurs
     */
    async getVersions() {
        try {
            let versions = new Array()
            let isLast = false;
            let startAt = 0;
            while (!isLast){
                let response = await this.request
                .get( this.config.baseUrl + '/rest/agile/latest/board/' + this.config.boardId + '/version?startAt=' + startAt)
                .auth(this.config.username, this.config.password);
                isLast = response.body.isLast;
                startAt = startAt + 50;
                if (response.body.values) {
                    response.body.values.forEach(version => {
                        versions.push(version);
                    });
                }
            }
            return versions;
        } catch (error) {
            console.error('Unable to get jira versions , error: ' + error);
            return null;
        }
    };

    /**
     * Gets the ID of a specific version, with option to create if not found
     * @param {string} version - The version name to find
     * @param {boolean} createIfNotExist - Whether to create the version if it doesn't exist
     * @returns {Promise<number|null>} - The version ID or null if not found/created
     */
    async getVersionId(version, createIfNotExist=true) {
        try {
            let response;
            let isLast = false;
            let startAt = 0;
            while (!isLast){
                response = await this.request
                .get( this.config.baseUrl + '/rest/agile/latest/board/' + this.config.boardId + '/version?startAt=' + startAt)
                .auth(this.config.username, this.config.password);
                isLast = response.body.isLast;
                startAt = startAt + 50;
                if (response.body.values) {
                    let index = response.body.values.findIndex((ver) => ver.name.toLowerCase() === version.toLowerCase());
                    if (index !== -1) {
                        return response.body.values[index].id;
                    }
                }
            }
            if (createIfNotExist) {
                let body = {
                    "description": '',
                    "name": version,
                    "userStartDate": this.getDate(),
                    "archived": false,
                    "released": false,
                    "project": this.config.projectKey
                };

                response = await this.request
                .post(this.config.baseUrl + '/rest/api/latest/version')
                .auth(this.config.username, this.config.password)
                .type('json')
                .send(body);
                if (response.body.id){
                    return Number(response.body.id);
                } else {
                    console.error('Version ID not found for version ' + version + ' and creation failed');
                    return null;
                }
            } else {
                return null;
            }
        } catch (error) {
            console.error('Unable to get jira Version id for version ' + version + ', error: ' + error);
            return null;
        }
    };

    /**
     * Deletes a version by ID
     * @param {string|number} versionId - The ID of the version to delete
     * @returns {Promise<Object|null>} - Response body or null if an error occurs
     */
    async deleteVersion(versionId) {
        let body = {};
        try {
            let response = await this.request
            .post(this.config.baseUrl + '/rest/api/latest/version/' + versionId + '/removeAndSwap')
            .auth(this.config.username, this.config.password)
            .type('json')
            .send(body);
            return response.body;
        } catch (error) {
            console.error('Unable to delete version: ' + versionId + ', ERROR: ' + error);
            return null;
        }
    };

    /**
     * Gets the ID of a specific issue by its key
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @returns {Promise<string|null>} - The issue ID or null if not found
     */
    async getIssueId(issueKey) {
        try {
            let response = await this.request
            .get( this.config.baseUrl + '/rest/api/latest/issue/' + issueKey)
            .auth(this.config.username, this.config.password);
            if (response.body.id) {
                return response.body.id;
            } else {
                console.error('Issue ID not found.');
                return null;
            }
        } catch (error) {
            console.error('Unable to get jira issue id for ' + issueKey + ', error: ' + error);
            return null;
        }
    };

    /**
     * Creates a new issue in Jira
     * @param {string} summary - The issue summary
     * @param {string} description - The issue description
     * @param {string} issueType - The issue type (e.g., Bug, Task, Story)
     * @param {Object} additionalFields - Additional fields to include in the issue
     * @returns {Promise<Object|null>} - Response containing the created issue or null if creation fails
     */
    async createIssue(summary, description, issueType = 'Bug', additionalFields = {}) {
        try {
            // Get project key from config
            const projectKey = this.config.projectKey;
            if (!projectKey) {
                throw new Error('Project key not configured');
            }

            console.log(`Creating Jira issue of type "${issueType}" with summary: "${summary}"`);

            // Create basic issue without looking up metadata first
            const body = {
                fields: {
                    project: {
                        key: projectKey
                    },
                    summary: summary,
                    description: description,
                    issuetype: {
                        name: issueType
                    },
                    ...additionalFields
                }
            };

            console.log('Sending create issue request with body:', JSON.stringify(body, null, 2));

            const response = await this.request
                .post(this.config.baseUrl + '/rest/api/latest/issue')
                .auth(this.config.username, this.config.password)
                .type('json')
                .send(body);
            
            console.log('Create issue response status:', response.status);
            console.log('Create issue response body:', JSON.stringify(response.body, null, 2));
            
            if (response.body && response.body.key) {
                console.log('Successfully created issue:', response.body.key);
                return response.body;
            } else {
                console.error('Failed to create issue, invalid response:', response.body);
                return null;
            }
        } catch (error) {
            console.error('Unable to create Jira issue, error details:', error.message);
            if (error.response) {
                console.error('Response status:', error.response.status);
                console.error('Response body:', error.response.body);
                
                // Check for specific error messages about missing required fields
                if (error.response.body && error.response.body.errors) {
                    console.error('Field errors:', error.response.body.errors);
                    
                    // If there are required fields, try to get the allowed values
                    const requiredFields = Object.keys(error.response.body.errors);
                    if (requiredFields.length > 0) {
                        try {
                            console.log('Attempting to get field information for:', requiredFields.join(', '));
                            // You can implement field lookup here if needed
                        } catch (metadataError) {
                            console.error('Failed to get field metadata:', metadataError.message);
                        }
                    }
                }
            }
            return null;
        }
    }

    /**
     * Deletes an issue by its key
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @returns {Promise<boolean>} - True if deletion was successful, false otherwise
     */
    async deleteIssue(issueKey) {
        try {
            let response = await this.request
                .delete(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey)
                .auth(this.config.username, this.config.password);
            
            return response.status === 204; // 204 No Content indicates success
        } catch (error) {
            console.error('Unable to delete jira issue, error: ' + error);
            return false;
        }
    };

    /**
     * Posts a comment to an issue
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @param {string} comment - The comment text
     * @returns {Promise<Object|null>} - Response containing the created comment or null if an error occurs
     */
    async postComment(issueKey, comment) {
        try {
            let response = await this.request
            .post(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey + '/comment')
            .auth(this.config.username, this.config.password)
            .type('json')
            .send({
                "body": comment
            });
            // return execution id
            return response.body;
        } catch (error) {
            console.error('Unable to post comment on jira issue, error: ' + error);
            return null;
        }
    };

    /**
     * Gets all issue types available for the configured project
     * @returns {Promise<Array<string>>} - Array of issue type names
     */
    async getProjectIssueTypes() {
        try {
            // Get available issue types for the project using the correct endpoint
            let response = await this.request
                .get(this.config.baseUrl + '/rest/api/latest/project/' + this.config.projectKey + '/issuetypes')
                .auth(this.config.username, this.config.password);
            
            if (response.body && Array.isArray(response.body)) {
                const issueTypes = response.body.map(type => type.name);
                console.log('Available issue types for project:', issueTypes);
                return issueTypes;
            } else {
                console.error('Failed to get issue types, invalid response:', response.body);
                return [];
            }
        } catch (error) {
            console.error('Unable to get project issue types, error details:', error.message);
            if (error.response) {
                console.error('Response status:', error.response.status);
                console.error('Response body:', error.response.body);
            }
            
            // Fallback: try to get all issue types if project-specific ones fail
            try {
                console.log('Falling back to getting all issue types...');
                let fallbackResponse = await this.request
                    .get(this.config.baseUrl + '/rest/api/latest/issuetype')
                    .auth(this.config.username, this.config.password);
                    
                if (fallbackResponse.body && Array.isArray(fallbackResponse.body)) {
                    const fallbackTypes = fallbackResponse.body.map(type => type.name);
                    console.log('Available issue types (fallback):', fallbackTypes);
                    return fallbackTypes;
                }
            } catch (fallbackError) {
                console.error('Fallback also failed:', fallbackError.message);
            }
            
            // Last resort: return common issue types that might work
            return ['Bug', 'Task', 'Story', 'Improvement', 'New Feature'];
        }
    }

    /**
     * Gets metadata for issue creation, including available issue types and fields
     * @returns {Promise<Object|null>} - Metadata object or null if retrieval fails
     */
    async getIssueCreateMetadata() {
        try {
            console.log('Getting issue create metadata for project:', this.config.projectKey);
            // Use the specific project metadata endpoint
            const response = await this.request
                .get(this.config.baseUrl + '/rest/api/latest/issue/createmeta')
                .query({ 
                    projectKeys: this.config.projectKey, 
                    expand: 'projects.issuetypes.fields',
                    issuetypes: 'Bug,Task,Story,New Feature'  // Specify common issue types
                })
                .auth(this.config.username, this.config.password);
            
            console.log('Metadata response status:', response.status);
            
            if (response.body && 
                response.body.projects && 
                response.body.projects.length > 0) {
                
                console.log('Available issue types:', 
                    response.body.projects[0].issuetypes.map(t => t.name).join(', '));
                return response.body.projects[0];
            } else {
                console.error('Failed to get issue create metadata, invalid response:', response.body);
                return null;
            }
        } catch (error) {
            console.error('Unable to get issue create metadata:', error.message);
            if (error.response) {
                console.error('Response status:', error.response.status);
                console.error('Response body:', JSON.stringify(error.response.body, null, 2));
            }
            return null;
        }
    }

    /**
     * Gets information about required fields for a specific issue type
     * @param {string} issueType - The issue type to get required fields for
     * @returns {Promise<Object|null>} - Object containing required fields info or null if retrieval fails
     */
    async getRequiredFields(issueType) {
        try {
            const metadata = await this.getIssueCreateMetadata();
            if (!metadata) {
                throw new Error('Could not retrieve issue create metadata');
            }

            const availableTypes = metadata.issuetypes || [];
            const selectedType = availableTypes.find(t => t.name === issueType);
            
            if (!selectedType) {
                throw new Error(`Issue type "${issueType}" not found`);
            }
            
            const fields = selectedType.fields || {};
            const requiredFields = {};
            
            Object.entries(fields).forEach(([fieldId, fieldDetails]) => {
                if (fieldDetails.required) {
                    requiredFields[fieldId] = {
                        name: fieldDetails.name,
                        description: fieldDetails.description,
                        type: fieldDetails.schema ? fieldDetails.schema.type : 'unknown',
                        allowedValues: fieldDetails.allowedValues
                    };
                }
            });
            
            return requiredFields;
        } catch (error) {
            console.error('Unable to get required fields:', error.message);
            return null;
        }
    }

    /**
     * Search for issues using JQL (Jira Query Language)
     * @param {string} jql - JQL query string
     * @param {number} maxResults - Maximum number of results to return
     * @param {Array<string>} fields - Array of field names to include in response
     * @returns {Promise<Object>} - Search results
     */
    async searchIssues(jql, maxResults = 50, fields = ['summary', 'status', 'priority']) {
        try {
            console.log(`Searching Jira issues with query: ${jql}`);
            
            const body = {
                jql: jql,
                maxResults: maxResults,
                fields: fields
            };

            let response = await this.request
                .post(this.config.baseUrl + '/rest/api/latest/search')
                .auth(this.config.username, this.config.password)
                .type('json')
                .send(body);
                
            if (response.body && response.body.issues) {
                console.log(`Found ${response.body.issues.length} issues`);
                return response.body;
            } else {
                console.error('No issues found or invalid response.');
                return null;
            }
        } catch (error) {
            console.error('Unable to search Jira issues, error: ' + error);
            return null;
        }
    };

    /**
     * Get available transitions for an issue
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @returns {Promise<Array>} - Available transitions
     */
    async getIssueTransitions(issueKey) {
        try {
            let response = await this.request
                .get(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey + '/transitions')
                .auth(this.config.username, this.config.password);
                
            if (response.body && response.body.transitions) {
                console.log(`Found ${response.body.transitions.length} transitions for issue ${issueKey}`);
                return response.body.transitions;
            } else {
                console.error('No transitions found or invalid response.');
                return [];
            }
        } catch (error) {
            console.error('Unable to get issue transitions, error: ' + error);
            return [];
        }
    };

    /**
     * Transition an issue to a new status
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @param {string} transitionId - The ID of the transition
     * @param {Object} fields - Optional fields to update during transition
     * @returns {Promise<boolean>} - Whether the transition was successful
     */
    async transitionIssue(issueKey, transitionId, fields = {}) {
        try {
            const body = {
                transition: {
                    id: transitionId
                }
            };
            
            // Add fields if provided
            if (Object.keys(fields).length > 0) {
                body.fields = fields;
            }

            let response = await this.request
                .post(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey + '/transitions')
                .auth(this.config.username, this.config.password)
                .type('json')
                .send(body);
                
            return response.status === 204; // 204 No Content indicates success
        } catch (error) {
            console.error('Unable to transition issue, error: ' + error);
            return false;
        }
    };

    /**
     * Add a label to an issue
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @param {string} label - The label to add
     * @returns {Promise<boolean>} - Whether the label was added successfully
     */
    async addLabel(issueKey, label) {
        try {
            // First get current labels
            let response = await this.request
                .get(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey + '?fields=labels')
                .auth(this.config.username, this.config.password);
                
            if (!response.body || !response.body.fields) {
                console.error('Failed to get issue labels.');
                return false;
            }
            
            // Get current labels or initialize empty array
            const currentLabels = response.body.fields.labels || [];
            
            // Don't add if label already exists
            if (currentLabels.includes(label)) {
                console.log(`Label "${label}" already exists on issue ${issueKey}`);
                return true;
            }
            
            // Add the new label
            const updatedLabels = [...currentLabels, label];
            
            // Update the issue with new labels
            const updateResponse = await this.request
                .put(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey)
                .auth(this.config.username, this.config.password)
                .type('json')
                .send({
                    fields: {
                        labels: updatedLabels
                    }
                });
                
            return updateResponse.status === 204; // 204 No Content indicates success
        } catch (error) {
            console.error('Unable to add label to issue, error: ' + error);
            return false;
        }
    };

    /**
     * Update issue fields
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @param {Object} fields - Fields to update
     * @returns {Promise<boolean>} - Whether the update was successful
     */
    async updateIssue(issueKey, fields) {
        try {
            console.log('Jira update Issue fields: ', fields)
            if (!fields || Object.keys(fields).length === 0) {
                console.error('No fields provided for update.');
                return false;
            }

            let response = await this.request
                .put(this.config.baseUrl + '/rest/api/latest/issue/' + issueKey)
                .auth(this.config.username, this.config.password)
                .type('json')
                .send({
                    fields: fields
                });
            return response.status === 204; // 204 No Content indicates success
        } catch (error) {
            console.error('Unable to update issue, error: ' + error);
            return false;
        }
    };
    async updateDescription(issueKey, text) {
        let fields = {
            "description": text
        }
        await this.updateIssue(issueKey, fields);
    }

    /**
     * Get issue details
     * @param {string} issueKey - The issue key (e.g., PROJ-123)
     * @param {Array<string>} fields - Specific fields to retrieve, empty for all fields
     * @returns {Promise<Object>} - Issue details
     */
    async getIssueDetails(issueKey, fields = []) {
        try {
            let url = this.config.baseUrl + '/rest/api/latest/issue/' + issueKey;
            
            // Add fields parameter if specified
            if (fields.length > 0) {
                url += '?fields=' + fields.join(',');
            }

            let response = await this.request
                .get(url)
                .auth(this.config.username, this.config.password);
                
            if (response.body) {
                return response.body;
            } else {
                console.error('Failed to get issue details.');
                return null;
            }
        } catch (error) {
            console.error('Unable to get issue details, error: ' + error);
            return null;
        }
    };
    async attachFileToIssue(issueKey, file){
        console.log('File '+file+' exist: ', await fs.existsSync(file));
        try {
            let url = this.config.baseUrl + '/rest/api/latest/issue/' + issueKey + '/attachments';
            console.log('URL:', url)
            let response = await this.request
                .post(url)
                .auth(this.config.username, this.config.password)
                .set('X-Atlassian-Token', 'no-check')
                .attach('file', fs.createReadStream(file))
            //console.log('Attachment response: ', response)
            if (response.body) {
                return response.body;
            } else {
                console.error('Failed to get issue details.');
                return null;
            }
        } catch (error) {
            console.error('Unable to upload file, error: ' + error);
            return null;
        }
    }
}
