from pydantic import BaseModel
from typing import Optional, List
from pydantic import BaseModel, Field, conint
from enum import Enum
import sys
import os

from puli_mcp_server.mcp_server.models import ChangeSet
from puli_models import (
    IncidentQueryResult,
    ChaosPatternQueryResult,
    BestPracticeQueryResult,
    SecurityQueryResult,
    RegulationQueryResult,
    DomainIndicatorQueryResult,
)


# ANSI color codes
class ANSIColors:
    """ANSI escape codes for terminal colors."""
    RED = '\033[91m'
    ORANGE = '\033[38;5;208m'  # Orange (256-color mode)
    YELLOW = '\033[93m'
    GREEN = '\033[92m'
    MAGENTA = '\033[95m'
    RESET = '\033[0m'


def _should_use_colors() -> bool:
    """
    Detect if ANSI colors should be used based on environment.

    Returns:
        True if colors should be used, False otherwise.
    """
    # Check for FORCE_COLOR override (takes precedence)
    if os.getenv("DISABLE_COLOR"):
        return False

    # Check if output is a TTY
    if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
        return True

    return False


def risk_meter(rate: int, total: int = 100, bar_length: int = 20, color: str = "") -> str:
    """
    Generate a risk meter progress bar.

    Args:
        rate: Current value (0-100)
        total: Maximum value (default: 100)
        bar_length: Number of characters in the bar (default: 20)
        color: ANSI color code to apply to the meter (optional)

    Returns:
        String with format: ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱
    """
    # Calculate how many filled blocks
    filled = int((rate / total) * bar_length)
    empty = bar_length - filled

    # Create the bar
    bar = '▰' * filled + '▱' * empty

    # Apply color if provided
    if color:
        return f"{color}{bar}{ANSIColors.RESET}"

    # Return formatted string
    return bar


def print_logo():
    return r"░░░██████╗░░██╗░░░██╗░██╗░░░░░░██╗░░░" + "\n" + \
           r"░░░██╔══██╗░██║░░░██║░██║░░░░░░██║░░░" + "\n" + \
           r"░░░██████╔╝░██║░░░██║░██║░░░░░░██║░░░" + "\n" + \
           r"░░░██╔═══╝░░██║░░░██║░██║░░░░░░██║░░░" + "\n" + \
           r"░░░██║░░░░░░╚██████╔╝░███████╗░██║░░░" + "\n" + \
           r"░░░╚═╝░░░░░░░╚═════╝░░╚══════╝░╚═╝░░░"


class LLMQueryRequest(BaseModel):
    """Request object for the LLM Agent query API."""
    company_profile: str | None
    domains: List[DomainIndicatorQueryResult]
    change_set: ChangeSet
    historical_incidents: List[IncidentQueryResult]
    relevant_chaos_patterns: List[ChaosPatternQueryResult]
    relevant_best_practices: List[BestPracticeQueryResult]
    relevant_security_items: List[SecurityQueryResult]
    relevant_regulations: List[RegulationQueryResult]

    def to_prompt_str(self) -> str:

        prompt = ""

        if self.company_profile:
            prompt += f"\nInformation about the company which this code belongs to: '{self.company_profile}'."

        # Add domain indicators
        if self.domains:
            domain_indicators_str = ', '.join([domain_indicator.id for domain_indicator in self.domains])
            prompt += f"\nThe product domains are: '{domain_indicators_str}'."

        if any([self.domains, self.company_profile]):
            prompt += f"\nMake sure to adjust your output to be in terms which are relevant to the"
            if self.company_profile:
                prompt += f" company"
                if self.domains:
                    prompt += f" and to the"
            if self.domains:
                prompt += f" domains"
            prompt += ".\n"

        prompt += "Analyze the following code changes:\n"
        # Change set
        prompt += self.change_set.to_embedding_string()

        # add code diff
        for change in self.change_set.changes:
            prompt += f"\n{change.to_str()}"

        # Add best practices
        best_practices_str_list = [best_practice.to_prompt_str() for best_practice in self.relevant_best_practices]
        security_items_str_list = [security_item.to_prompt_str() for security_item in self.relevant_security_items]
        regulations_str_list = [regulation.to_prompt_str() for regulation in self.relevant_regulations]
        chaos_patterns_str_list = [chaos_pattern.to_prompt_str() for chaos_pattern in self.relevant_chaos_patterns]
        incidents_str_list = [incident.to_prompt_str() for incident in self.historical_incidents]

        if any([best_practices_str_list, security_items_str_list, regulations_str_list, chaos_patterns_str_list, incidents_str_list]):
            prompt += "\nRelated context that can be useful for the analysis:"

        if best_practices_str_list:
            prompt += "\nBest practices to focus on:"
            for ind, best_practice_str in enumerate(best_practices_str_list):
                prompt += f"\n{ind}. {best_practice_str}"

        # Add security items
        if security_items_str_list:
            prompt += "\nSecurity violations to verify:"
            for ind, security_item_str in enumerate(security_items_str_list):
                prompt += f"\n{ind}. {security_item_str}"

        # Add regulations
        if regulations_str_list:
            prompt += "\nRegulations to consider:"
            for ind, regulation_str in enumerate(regulations_str_list):
                prompt += f"\n{ind}. {regulation_str}"

        # Add chaos patterns
        if chaos_patterns_str_list:
            prompt += "\nChaos patterns to consider when testing the changes:"
            for ind, chaos_pattern_str in enumerate(chaos_patterns_str_list):
                prompt += f"\n{ind}. {chaos_pattern_str}"

        # Add incidents
        if incidents_str_list:
            prompt += "\nRelated past incidents to use as a reference:"
            for ind, incident_str in enumerate(incidents_str_list):
                prompt += f"\n{ind}. {incident_str}"

        return prompt


# Icon mapping for risk assessment levels
_RISK_LEVEL_ICONS = {
    "CRITICAL": "🔴 Critical",
    "HIGH_RISK": "🟠 Major",
    "MODERATE": "🟡 Moderate",
    "LOW_RISK": "🟢 Low",
}


class RiskAssessmentLevel(str, Enum):
    CRITICAL = "CRITICAL"
    HIGH_RISK = "HIGH_RISK"
    MODERATE = "MODERATE"
    LOW_RISK = "LOW_RISK"

    def to_icon(self) -> str:
        return _RISK_LEVEL_ICONS.get(self.value, "❓")


class TechnicalFinding(BaseModel):
    """
    Precise, factual, code-level details of the finding.
    """
    file_path: str = Field(..., description="The relative path to the file.")
    line_number: str = Field(..., description="The specific line number or range (e.g., '47' or '47-52').")
    change_description: str = Field(..., description="Brief summary of what changed in the code.")
    technical_reason: str = Field(
        ..., 
        description="Why this breaks. Must be technical and specific (e.g., 'Missing index causes table scan'). Avoid vague phrases like 'might cause issues'."
    )

class BusinessContext(BaseModel):
    """
    Contextualizes the code within the broader business process.
    Requirement: You must break the flow down into atomic steps.
    Requirement: One step must clearly identify where the failure occurs (e.g., ["User Clicks", "API Request", "[DB DEADLOCK]", "Response Timeout"]).
    """
    process_description: str = Field(..., description="A description of the business process served by this code.")
    flow_steps: List[str] = Field(
        ..., 
        description="List of steps in the process. The point of failure should be one of the steps. Add a marker on the step that"
    )

class RealIncident(BaseModel):
    """
    Historical context if a similar pattern has caused a major outage before.
    """
    company: str = Field(..., description="The name of the company that suffered the incident.")
    year: str = Field(..., description="The year the incident occurred.")
    description: str = Field(..., description="Brief description of what happened in that specific incident.")

class RiskAssessment(BaseModel):
    """
    The main structure for the code review output.
    """
    risk_assessment_level: RiskAssessmentLevel = Field(
        ..., 
        description="The level of risk associated with the code change."
    )
    risk_score: conint(ge=0, le=100) = Field(
        ..., 
        description="Risk score from 0 (Critical) to 100 (Safe)."
    )
    business_flow_name: str = Field(..., description="Top level name of the flow (e.g. 'Checkout Process').")
    technical_finding: TechnicalFinding
    business_context: BusinessContext
    consequence: str = Field(
        ..., 
        description="Description: What happens to the user or the business. Style Rule: Be strictly factual. No drama. No hyperbole. Example: \"User is double-charged. Support ticket generated.\" (NOT \"Catastrophic failure destroys trust\")."
    )
    chaos_scenario: Optional[str] = Field(
        None,
        description="Description of the chaos scenario that was run to test this risk assessment. your response should answer the question: What if this scenario happens? Start with the words: 'What happends if ...'"
    )
    historical_incident: Optional[RealIncident] = Field(
        None, 
        description="Only populate if a famous/known incident matches this exact failure pattern."
    )
    closing_line: str = Field(
        ..., 
        description="A short line describing the risk assessment. No more then 15 words, typically 7 words."
    )

    def to_str(self, use_colors: Optional[bool] = None) -> str:
            """
            Converts the object into the specific text format required for the prompt.

            Args:
                use_colors: Whether to use ANSI colors. If None, auto-detect based on TTY.
            """
            # Auto-detect color support if not explicitly specified
            if use_colors is None:
                use_colors = _should_use_colors()

            sep = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

            # Conditionally apply colors
            colored_sep = sep
            risk_color = ""
            if use_colors:
                colored_sep = f"{ANSIColors.MAGENTA}{sep}{ANSIColors.RESET}"
                # Map risk levels to colors
                risk_colors = {
                    "CRITICAL": ANSIColors.RED,
                    "HIGH_RISK": ANSIColors.ORANGE,
                    "MODERATE": ANSIColors.YELLOW,
                    "LOW_RISK": ANSIColors.GREEN,
                }
                risk_color = risk_colors.get(self.risk_assessment_level.value, "")

            # Build the sections list
            sections = []

            # Header Section
            sections.append(colored_sep)
            icon = self.risk_assessment_level.to_icon()
            meter = risk_meter(100 -self.risk_score, color=risk_color)

            risk_assessment_level_str = f"{icon}"
            if use_colors:
                risk_assessment_level_str = f"{risk_color}{icon} {ANSIColors.RESET}"

            sections.append(f"Puli Risk Assessment: {risk_assessment_level_str}")
            sections.append(f"{meter}")
            sections.append(colored_sep)

            # Technical Finding
            sections.append("[TECHNICAL FINDING]")
            tf = self.technical_finding
            sections.append(f"File: {tf.file_path}, Line: {tf.line_number}")
            sections.append(f"Change: {tf.change_description}")
            sections.append(f"Why it breaks: {tf.technical_reason}\n")

            # What If section
            sections.append("[WHAT IF]")
            sections.append(f"{self.chaos_scenario}\n")

            # Consequence
            sections.append("[CONSEQUENCE]")
            sections.append(f"{self.consequence}\n")

            # Business Flow
            sections.append("[BUSINESS FLOW]")
            bc = self.business_context
            sections.append(f"Process: {bc.process_description}")

            # JOIN LOGIC: Join the list with arrows
            formatted_flow = " → ".join(bc.flow_steps)
            sections.append(f"Flow: {formatted_flow}\n")

            # 5. Real Incident (Only if present)
            if self.historical_incident:
                inc = self.historical_incident
                sections.append("[REAL INCIDENT]")
                sections.append(f"{inc.company} ({inc.year}): {inc.description}")
                sections.append(colored_sep)
            else:
                sections.append(colored_sep)

            # 6. Closing Line
            sections.append(f"{self.closing_line}")
            sections.append(colored_sep)

            return "\n".join(sections)


class RiskAssessmentList(BaseModel):
    """
    The main structure for the code review output.
    """
    risk_assessments: List[RiskAssessment] = Field(
        ..., 
        description="List of risk assessments for the code change."
    , default_factory=list)

    def to_str(self, use_colors: Optional[bool] = None) -> str:
            """
            Converts the object into the specific text format required for the prompt.

            Args:
                use_colors: Whether to use ANSI colors. If None, auto-detect based on TTY.
            """
            # Auto-detect color support if not explicitly specified
            if use_colors is None:
                use_colors = _should_use_colors()


            sep = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
            # Conditionally apply colors
            colored_sep = sep
            critical_icon = _RISK_LEVEL_ICONS[RiskAssessmentLevel.CRITICAL]
            high_icon = _RISK_LEVEL_ICONS[RiskAssessmentLevel.HIGH_RISK]
            moderate_icon = _RISK_LEVEL_ICONS[RiskAssessmentLevel.MODERATE]
            low_icon = _RISK_LEVEL_ICONS[RiskAssessmentLevel.LOW_RISK]
            if use_colors:
                critical_icon = f"{ANSIColors.RED}{critical_icon} {ANSIColors.RESET}"
                high_icon = f"{ANSIColors.ORANGE}{high_icon} {ANSIColors.RESET}"
                moderate_icon = f"{ANSIColors.YELLOW}{moderate_icon} {ANSIColors.RESET}"
                low_icon = f"{ANSIColors.GREEN}{low_icon} {ANSIColors.RESET}"
                colored_sep = f"{ANSIColors.MAGENTA}{sep}{ANSIColors.RESET}"

            critical_risk_assessments = [risk_assessment for risk_assessment in self.risk_assessments if risk_assessment.risk_assessment_level == RiskAssessmentLevel.CRITICAL]
            high_risk_assessments = [risk_assessment for risk_assessment in self.risk_assessments if risk_assessment.risk_assessment_level == RiskAssessmentLevel.HIGH_RISK]
            moderate_risk_assessments = [risk_assessment for risk_assessment in self.risk_assessments if risk_assessment.risk_assessment_level == RiskAssessmentLevel.MODERATE]
            low_risk_assessments = [risk_assessment for risk_assessment in self.risk_assessments if risk_assessment.risk_assessment_level == RiskAssessmentLevel.LOW_RISK]
            risks_amount = [len(critical_risk_assessments), len(high_risk_assessments), len(moderate_risk_assessments), len(low_risk_assessments)]

            # Build the sections list
            sections = []

            # Header Section
            sections.append(colored_sep)
            sections.append(print_logo())
            sections.append(colored_sep)

            risks = []
            if risks_amount[0]:
                risks.append(f" {risks_amount[0]} {critical_icon}")
            if risks_amount[1]:
                risks.append(f"{risks_amount[1]} {high_icon}")
            if risks_amount[2]:
                risks.append(f"{risks_amount[2]} {moderate_icon}")
            if risks_amount[3]:
                risks.append(f"{risks_amount[3]} {low_icon}")
            summery_str = f"Puli Risk Assemsnent Summary: {', '.join(risks)}"
            sections.append(summery_str)
            sections.append(colored_sep)
            for risk_assessment in self.risk_assessments:
                sections.append(risk_assessment.to_str(use_colors))
            return "\n".join(sections)

"""

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
░░░██████╗░░██╗░░░██╗░██╗░░░░░░██╗░░░
░░░██╔══██╗░██║░░░██║░██║░░░░░░██║░░░
░░░██████╔╝░██║░░░██║░██║░░░░░░██║░░░
░░░██╔═══╝░░██║░░░██║░██║░░░░░░██║░░░
░░░██║░░░░░░╚██████╔╝░███████╗░██║░░░
░░░╚═╝░░░░░░░╚═════╝░░╚══════╝░╚═╝░░░
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Puli Risk Assessment: ❗ 
▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[TECHNICAL FINDING]
File: fe/components/settings/add-user-sheet.tsx, Line: ~1-238
Change: Introduces client-side user creation without submission locking or idempotency protection
Why it breaks: The form allows multiple rapid submissions (double-click, retry on slow network). createUser() posts to POST /users/ without a client-generated idempotency key or disabled submit tied to request lifecycle. If the backend is non-idempotent (typical for user creation), duplicate users can be created.

[WHAT IF]
What happends if the admin double-clicks submit or the network retries the POST while the first request is still in flight?

[CONSEQUENCE]
Two or more identical user accounts are created. Cleanup requires manual intervention and may break login or email uniqueness constraints.

[BUSINESS FLOW]
Process: Admin creates a new user from settings
Flow: Open Settings → Open Add User → Fill Form → Submit → [DUPLICATE REQUEST] POST /users → Users List Refresh

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Plausible duplicate creation under real network conditions.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""