"""
ADO Integration - Handles all Azure DevOps API communication.
Fetches test cases and work item details from ADO.
"""

import requests
import os
import re
import html
from typing import List, Dict, Any, Optional, Union
from dotenv import load_dotenv

from core.logger import get_logger
from services.tag_service import filter_test_case_tags

load_dotenv()


class ADOIntegration:
    """
    Integration service for Azure DevOps API communication.
    Fetches and parses test cases from ADO.
    """

    DEFAULT_EXPECTED_RESULT = "No expected result defined."
    
    ACTION_STEP_PATTERN = re.compile(r'<step id="\d+" type="ActionStep">(.*?)</step>', re.DOTALL)
    DIV_PATTERN = re.compile(r'<div>(?:<div>)?.*?(?:</div>)?</div>', re.DOTALL)
    PARAGRAPH_PATTERN = re.compile(r'<p>.*?</p>', re.DOTALL)

    def __init__(self, logger=None):
        """Initialize ADO integration with credentials from environment."""
        self.logger = logger or get_logger(__name__)
        self.ado_org = os.getenv("ADO_ORG")
        self.ado_project = os.getenv("ADO_PROJECT")
        self.ado_pat = os.getenv("ADO_PAT")
        self.ado_api_version = "7.1-preview.3"
        self.content_type_json = "application/json"

    def get_ado_headers(self) -> Dict[str, str]:
        """Get headers for Azure DevOps API requests."""
        if not self.ado_pat:
            raise ValueError("ADO_PAT is not set in the environment variables.")
        return {"Content-Type": self.content_type_json}

    def _make_ado_request(self, url: str, operation_description: str) -> Dict[str, Any]:
        """Make an authenticated request to Azure DevOps API."""
        try:
            response = requests.get(url, headers=self.get_ado_headers(), auth=("", self.ado_pat))
            response.raise_for_status()
            return response.json()
        except requests.exceptions.HTTPError as e:
            self.logger.error(f"[ADO Error] HTTP Error {operation_description}: {e.response.status_code} - {e.response.text}")
            raise
        except Exception as e:
            self.logger.error(f"[ADO Error] Failed {operation_description}: {e}")
            raise

    def get_test_cases_from_suite(self, plan_id: str, suite_id: str) -> List[Dict[str, Any]]:
        """Fetch a list of test case references from a specific test suite."""
        url = f"https://dev.azure.com/{self.ado_org}/{self.ado_project}/_apis/test/Plans/{plan_id}/suites/{suite_id}/testcases?api-version={self.ado_api_version}"
        self.logger.info(f"[ADO] Fetching test cases from Plan ID: {plan_id}, Suite ID: {suite_id}")
        
        data = self._make_ado_request(url, f"fetching test cases from Plan ID: {plan_id}, Suite ID: {suite_id}")
        test_cases = data.get("value", [])
        self.logger.info(f"[ADO] Found {len(test_cases)} test cases in suite.")
        return test_cases

    def get_work_item_details(self, work_item_id: str) -> Dict[str, Any]:
        """Fetch the detailed information for a single work item (like a test case)."""
        url = f"https://dev.azure.com/{self.ado_org}/{self.ado_project}/_apis/wit/workitems/{work_item_id}?$expand=all&api-version={self.ado_api_version}"
        return self._make_ado_request(url, f"fetching work item {work_item_id}")

    def clean_html_content(self, text: Optional[str]) -> str:
        """Clean HTML content by removing tags and decoding entities."""
        if not text:
            return ""
        
        cleaned_text = html.unescape(text)
        cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text)
        cleaned_text = re.sub(r'\s+', ' ', cleaned_text)
        cleaned_text = re.sub(r'\n+', '\n', cleaned_text)
        cleaned_text = cleaned_text.replace('BR/', '').replace('BR', '')
        return cleaned_text.strip()

    def parse_steps_from_html(self, html_steps: Optional[str]) -> List[str]:
        """Parse test steps from the HTML format provided by ADO, ensuring the last step is always included."""
        if not html_steps:
            return []

        parsed_steps = self._extract_action_steps(html_steps)
        if not parsed_steps:
            parsed_steps = self._extract_div_or_paragraph_steps(html_steps)
        
        return [step for step in parsed_steps if step.strip()]

    def _extract_action_steps(self, html_steps: str) -> List[str]:
        """Extract steps from ActionStep tags."""
        parsed_steps = []
        matched_spans = []
        
        for match in self.ACTION_STEP_PATTERN.finditer(html_steps):
            step_html = match.group(1)
            clean_step = self.clean_html_content(step_html)
            if clean_step:
                parsed_steps.append(clean_step)
            matched_spans.append(match.span())
        
        if matched_spans:
            trailing_text = self._extract_trailing_text(html_steps, matched_spans[-1][1])
            if trailing_text:
                parsed_steps.append(trailing_text)
        
        return parsed_steps

    def _extract_div_or_paragraph_steps(self, html_steps: str) -> List[str]:
        """Extract steps from div or paragraph tags."""
        parsed_steps = []
        matched_spans = []
        
        div_matches = list(self.DIV_PATTERN.finditer(html_steps))
        if div_matches:
            matched_spans = [match.span() for match in div_matches]
            for match in div_matches:
                clean_step = self.clean_html_content(match.group(0))
                if clean_step:
                    parsed_steps.append(clean_step)
        else:
            paragraph_matches = list(self.PARAGRAPH_PATTERN.finditer(html_steps))
            matched_spans = [match.span() for match in paragraph_matches]
            for match in paragraph_matches:
                clean_step = self.clean_html_content(match.group(0))
                if clean_step:
                    parsed_steps.append(clean_step)
        
        if matched_spans:
            trailing_text = self._extract_trailing_text(html_steps, matched_spans[-1][1])
            if trailing_text:
                parsed_steps.append(trailing_text)
        
        return parsed_steps

    def _extract_trailing_text(self, html_steps: str, last_end: int) -> Optional[str]:
        """Extract and clean trailing text after the last match."""
        trailing = html_steps[last_end:]
        return self.clean_html_content(trailing) if trailing.strip() else None

    def _normalize_testcase_ids(self, testcase_ids: Optional[Union[str, List[str]]]) -> Optional[List[str]]:
        """Normalize test case IDs from string or list format."""
        if not testcase_ids:
            return None
        
        if isinstance(testcase_ids, str):
            return [tc_id.strip() for tc_id in testcase_ids.split(',') if tc_id.strip()]
        
        return testcase_ids if isinstance(testcase_ids, list) else None

    def _get_test_case_refs(self, plan_id: str, suite_id: str, testcase_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
        """Get test case references either from suite or from specific IDs."""
        if testcase_ids:
            self.logger.info(f"[ADO] Fetching specific Test Case IDs: {testcase_ids}")
            return [{"testCase": {"id": tc_id}} for tc_id in testcase_ids]
        
        test_case_refs = self.get_test_cases_from_suite(plan_id, suite_id)
        if not test_case_refs:
            self.logger.info("[ADO] No test cases found in the specified suite.")
        return test_case_refs

    def _parse_tags_from_fields(self, fields: Dict[str, Any]) -> List[str]:
        """Parse tags from ADO fields (semicolon-separated string)."""
        tags_raw = fields.get("System.Tags", "") or ""
        return [tag.strip() for tag in tags_raw.split(";") if tag.strip()]

    def _get_expected_result(self, fields: Dict[str, Any]) -> str:
        """Get expected result from fields, checking multiple possible fields."""
        expected_result = fields.get("Microsoft.VSTS.TCM.AcceptanceCriteria", self.DEFAULT_EXPECTED_RESULT)
        if not expected_result or expected_result.strip() == '':
            expected_result = fields.get("Microsoft.VSTS.TCM.ExpectedResult", self.DEFAULT_EXPECTED_RESULT)
        return expected_result

    def _parse_single_test_case(self, ref: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """Parse a single test case from a reference."""
        test_case_id = ref.get("testCase", {}).get("id")
        if not test_case_id:
            return None

        self.logger.info(f"[ADO] Fetching details for Test Case ID: {test_case_id}")
        work_item = self.get_work_item_details(test_case_id)
        fields = work_item.get("fields", {})
        
        title = fields.get("System.Title", "No Title")
        description = fields.get("System.Description", "")
        tags_list = self._parse_tags_from_fields(fields)
        steps_html = fields.get("Microsoft.VSTS.TCM.Steps", "")
        expected_result = self._get_expected_result(fields)
        steps = self.parse_steps_from_html(steps_html)

        if not steps:
            self.logger.warning(f"[Warning] No steps found for Test Case ID: {test_case_id} ('{title}')")
        
        test_case = {
            "id": test_case_id,
            "title": title,
            "description": self.clean_html_content(description),
            "tags": tags_list,
            "metadata": {"tags": tags_list},
            "steps": steps,
            "expected_result": expected_result
        }
        
        test_case = filter_test_case_tags(test_case)
        self.logger.info(f"[ADO] Successfully parsed Test Case: '{title}'")
        return test_case

    def fetch_and_parse_test_cases(
        self, 
        plan_id: str, 
        suite_id: str, 
        testcase_ids: Optional[Union[str, List[str]]] = None
    ) -> List[Dict[str, Any]]:
        """
        Main function to fetch test cases.
        - If testcase_ids is None -> fetch all from suite.
        - If testcase_ids is a string (can be comma-separated) or a list -> fetch only those IDs.
        """
        normalized_ids = self._normalize_testcase_ids(testcase_ids)
        test_case_refs = self._get_test_case_refs(plan_id, suite_id, normalized_ids)
        
        parsed_test_cases = []
        for ref in test_case_refs:
            test_case = self._parse_single_test_case(ref)
            if test_case:
                parsed_test_cases.append(test_case)
        
        return parsed_test_cases


# Backward compatibility function
_ado_integration = ADOIntegration()


def fetch_and_parse_test_cases(
    plan_id: str, 
    suite_id: str, 
    testcase_ids: Optional[Union[str, List[str]]] = None
) -> List[Dict[str, Any]]:
    """Backward compatibility function."""
    return _ado_integration.fetch_and_parse_test_cases(plan_id, suite_id, testcase_ids)


__all__ = ["ADOIntegration", "fetch_and_parse_test_cases"]
