"""Keyword scoring engine — popularity, difficulty, and opportunity scoring.

Scoring methodology inspired by publicly documented ASO research.
Uses only iTunes Search API data (result count, ratings, title matches, etc.)
to estimate popularity and difficulty without paid API access.
"""

import math
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional

from .itunes import SearchResult, AppResult


@dataclass
class PopularityBreakdown:
    """Breakdown of popularity score components."""
    result_count_score: float       # 0-25: how many apps appear
    leader_strength_score: float    # 0-30: rating volume of top apps
    title_match_score: float        # 0-20: how many titles contain keyword
    market_depth_score: float       # 0-10: strong apps deep in results
    specificity_penalty: float      # -30 to 0: generic term adjustment
    exact_phrase_bonus: float       # 0-15: multi-word exact match reward
    total: float                    # 1-100 clamped

    def to_dict(self) -> dict:
        return {
            "result_count": round(self.result_count_score, 1),
            "leader_strength": round(self.leader_strength_score, 1),
            "title_match": round(self.title_match_score, 1),
            "market_depth": round(self.market_depth_score, 1),
            "specificity_penalty": round(self.specificity_penalty, 1),
            "exact_phrase_bonus": round(self.exact_phrase_bonus, 1),
            "total": round(self.total),
        }


@dataclass
class DifficultyBreakdown:
    """Breakdown of difficulty score components."""
    rating_volume_score: float      # 30% weight: how many ratings competitors have
    dominant_players_score: float   # 20% weight: presence of 100K+ rating apps
    rating_quality_score: float     # 10% weight: avg star rating of competitors
    market_maturity_score: float    # 10% weight: how old competitors are
    publisher_diversity_score: float # 10% weight: many publishers vs few
    app_count_score: float          # 10% weight: total results
    content_relevance_score: float  # 10% weight: how well results match keyword
    total: float                    # 1-100 clamped

    def to_dict(self) -> dict:
        return {
            "rating_volume": round(self.rating_volume_score, 1),
            "dominant_players": round(self.dominant_players_score, 1),
            "rating_quality": round(self.rating_quality_score, 1),
            "market_maturity": round(self.market_maturity_score, 1),
            "publisher_diversity": round(self.publisher_diversity_score, 1),
            "app_count": round(self.app_count_score, 1),
            "content_relevance": round(self.content_relevance_score, 1),
            "total": round(self.total),
        }


@dataclass
class KeywordScore:
    """Complete keyword analysis with all scores."""
    keyword: str
    country: str
    popularity: PopularityBreakdown
    difficulty: DifficultyBreakdown
    opportunity: float              # 1-100 composite
    opportunity_label: str          # "Sweet Spot", "Hidden Gem", etc.
    result_count: int
    top_apps: list[dict]
    download_estimates: dict

    def to_dict(self) -> dict:
        return {
            "keyword": self.keyword,
            "country": self.country,
            "popularity": self.popularity.to_dict(),
            "difficulty": self.difficulty.to_dict(),
            "opportunity_score": round(self.opportunity),
            "opportunity_label": self.opportunity_label,
            "result_count": self.result_count,
            "top_apps": self.top_apps,
            "download_estimates": self.download_estimates,
        }


def _app_age_days(app: AppResult) -> Optional[int]:
    """Calculate app age in days from release date."""
    try:
        rd = datetime.fromisoformat(app.release_date.replace("Z", "+00:00"))
        return (datetime.now(timezone.utc) - rd).days
    except Exception:
        return None


def _keyword_in_name(keyword: str, app_name: str) -> bool:
    """Check if keyword appears in app name (case-insensitive)."""
    return keyword.lower() in app_name.lower()


def score_popularity(result: SearchResult) -> PopularityBreakdown:
    """Calculate popularity score (1-100) from search results.
    
    6-signal model estimating how frequently a keyword is searched.
    """
    apps = result.apps
    count = result.result_count
    keyword = result.keyword.lower()
    word_count = len(keyword.split())

    # Signal 1: Result count (0-25)
    # More results = more apps competing = higher search volume
    if count >= 200:
        rc_score = 25.0
    elif count >= 100:
        rc_score = 15.0 + (count - 100) / 100 * 10
    elif count >= 50:
        rc_score = 8.0 + (count - 50) / 50 * 7
    elif count >= 10:
        rc_score = 2.0 + (count - 10) / 40 * 6
    else:
        rc_score = count / 10 * 2

    # Signal 2: Leader strength (0-30)
    # Top apps with lots of ratings = high-traffic keyword
    top_10 = apps[:10]
    if top_10:
        max_ratings = max(a.rating_count for a in top_10)
        avg_top_ratings = sum(a.rating_count for a in top_10[:5]) / min(5, len(top_10))

        if max_ratings >= 500_000:
            ls_score = 30.0
        elif max_ratings >= 100_000:
            ls_score = 22.0 + (max_ratings - 100_000) / 400_000 * 8
        elif max_ratings >= 10_000:
            ls_score = 12.0 + (max_ratings - 10_000) / 90_000 * 10
        elif max_ratings >= 1_000:
            ls_score = 5.0 + (max_ratings - 1_000) / 9_000 * 7
        else:
            ls_score = max_ratings / 1_000 * 5
    else:
        ls_score = 0.0

    # Signal 3: Title match density (0-20)
    # More apps with keyword in title = established search term
    title_matches = sum(1 for a in apps[:50] if _keyword_in_name(keyword, a.name))
    match_ratio = title_matches / max(len(apps[:50]), 1)
    tm_score = min(20.0, match_ratio * 100)

    # Signal 4: Market depth (0-10)
    # Strong apps appearing deep in results = lots of competition for this term
    deep_apps = apps[10:50]
    deep_strong = sum(1 for a in deep_apps if a.rating_count >= 1000)
    md_score = min(10.0, deep_strong * 2.0)

    # Signal 5: Specificity penalty (-30 to 0)
    # Single generic words inflate counts without indicating real search intent
    if word_count == 1 and count >= 150:
        # Likely a generic term — penalise
        sp_penalty = -min(30.0, (count - 150) / 50 * 15 + 10)
    elif word_count == 1 and count >= 100:
        sp_penalty = -5.0
    else:
        sp_penalty = 0.0

    # Signal 6: Exact phrase bonus (0-15)
    # Multi-word keywords with exact title matches show real search demand
    if word_count >= 2:
        exact_title = sum(1 for a in apps[:20] if keyword in a.name.lower())
        ep_bonus = min(15.0, exact_title * 3.0)
    else:
        ep_bonus = 0.0

    raw = rc_score + ls_score + tm_score + md_score + sp_penalty + ep_bonus
    total = max(1.0, min(100.0, raw))

    return PopularityBreakdown(
        result_count_score=rc_score,
        leader_strength_score=ls_score,
        title_match_score=tm_score,
        market_depth_score=md_score,
        specificity_penalty=sp_penalty,
        exact_phrase_bonus=ep_bonus,
        total=total,
    )


def score_difficulty(result: SearchResult) -> DifficultyBreakdown:
    """Calculate difficulty score (1-100) from search results.
    
    7-factor weighted model estimating how hard it is to rank.
    """
    apps = result.apps
    keyword = result.keyword.lower()
    count = result.result_count
    top_10 = apps[:10]
    top_20 = apps[:20]

    # Factor 1: Rating volume (30% weight)
    # High aggregate ratings = harder to compete
    if top_10:
        avg_ratings = sum(a.rating_count for a in top_10) / len(top_10)
        if avg_ratings >= 100_000:
            rv = 100.0
        elif avg_ratings >= 10_000:
            rv = 60.0 + (avg_ratings - 10_000) / 90_000 * 40
        elif avg_ratings >= 1_000:
            rv = 25.0 + (avg_ratings - 1_000) / 9_000 * 35
        elif avg_ratings >= 100:
            rv = 8.0 + (avg_ratings - 100) / 900 * 17
        else:
            rv = avg_ratings / 100 * 8
    else:
        rv = 0.0

    # Factor 2: Dominant players (20% weight)
    # Apps with 100K+ ratings are nearly impossible to dislodge
    dominant = sum(1 for a in top_10 if a.rating_count >= 100_000)
    dp = min(100.0, dominant * 25.0)

    # Factor 3: Rating quality (10% weight)
    # High-rated competitors = hard to differentiate
    if top_10:
        avg_stars = sum(a.rating for a in top_10 if a.rating > 0) / max(1, sum(1 for a in top_10 if a.rating > 0))
        rq = min(100.0, (avg_stars / 5.0) * 100)
    else:
        rq = 0.0

    # Factor 4: Market maturity (10% weight)
    # Old, established apps = entrenched market
    ages = [_app_age_days(a) for a in top_10]
    valid_ages = [a for a in ages if a is not None]
    if valid_ages:
        avg_age = sum(valid_ages) / len(valid_ages)
        if avg_age >= 2000:  # ~5.5 years
            mm = 100.0
        elif avg_age >= 1000:  # ~2.7 years
            mm = 50.0 + (avg_age - 1000) / 1000 * 50
        elif avg_age >= 365:
            mm = 20.0 + (avg_age - 365) / 635 * 30
        else:
            mm = avg_age / 365 * 20
    else:
        mm = 50.0  # unknown = assume moderate

    # Factor 5: Publisher diversity (10% weight)
    # Few publishers dominating = harder to break in
    if top_20:
        developers = set(a.developer for a in top_20)
        diversity_ratio = len(developers) / len(top_20)
        # Low diversity (few publishers) = harder
        pd = (1 - diversity_ratio) * 100
    else:
        pd = 0.0

    # Factor 6: App count (10% weight)
    if count >= 200:
        ac = 100.0
    elif count >= 100:
        ac = 50.0 + (count - 100) / 100 * 50
    elif count >= 50:
        ac = 20.0 + (count - 50) / 50 * 30
    else:
        ac = count / 50 * 20

    # Factor 7: Content relevance (10% weight)
    # How well top results match the keyword — irrelevant results = less competition
    if top_10:
        relevant = sum(1 for a in top_10 if _keyword_in_name(keyword, a.name) or
                       keyword in a.description[:200].lower())
        cr = (relevant / len(top_10)) * 100
    else:
        cr = 0.0

    # Weighted total
    raw = (
        rv * 0.30 +
        dp * 0.20 +
        rq * 0.10 +
        mm * 0.10 +
        pd * 0.10 +
        ac * 0.10 +
        cr * 0.10
    )
    total = max(1.0, min(100.0, raw))

    return DifficultyBreakdown(
        rating_volume_score=rv,
        dominant_players_score=dp,
        rating_quality_score=rq,
        market_maturity_score=mm,
        publisher_diversity_score=pd,
        app_count_score=ac,
        content_relevance_score=cr,
        total=total,
    )


def _difficulty_label(score: float) -> str:
    if score < 16:
        return "Very Easy"
    elif score < 36:
        return "Easy"
    elif score < 56:
        return "Moderate"
    elif score < 76:
        return "Hard"
    elif score < 91:
        return "Very Hard"
    return "Extreme"


def _opportunity_label(pop: float, diff: float) -> str:
    """Classify keyword opportunity based on popularity vs difficulty."""
    if pop >= 50 and diff <= 35:
        return "Sweet Spot"
    elif pop >= 30 and diff <= 25:
        return "Hidden Gem"
    elif pop >= 60 and diff <= 55:
        return "Competitive Opportunity"
    elif pop < 20 and diff <= 20:
        return "Low Volume"
    elif pop < 20:
        return "Not Worth It"
    elif diff >= 75:
        return "Avoid — Too Competitive"
    return "Worth Investigating"


def estimate_downloads(popularity: float, position: int) -> dict:
    """Estimate daily downloads for a ranking position.
    
    Pipeline: popularity → daily searches → tap-through → conversion
    """
    # Popularity to estimated daily searches (piecewise linear)
    if popularity >= 80:
        daily_searches = 5000 + (popularity - 80) / 20 * 15000
    elif popularity >= 60:
        daily_searches = 1000 + (popularity - 60) / 20 * 4000
    elif popularity >= 40:
        daily_searches = 200 + (popularity - 40) / 20 * 800
    elif popularity >= 20:
        daily_searches = 30 + (popularity - 20) / 20 * 170
    else:
        daily_searches = max(1, popularity / 20 * 30)

    # Position to tap-through rate (power-law decay)
    if position == 1:
        ttr = 0.30
    elif position <= 3:
        ttr = 0.15 / position
    elif position <= 10:
        ttr = 0.08 / math.log2(position + 1)
    elif position <= 20:
        ttr = 0.02 / math.log2(position)
    else:
        ttr = 0.001

    # Conversion rate range for free apps
    conv_low = 0.35
    conv_high = 0.55

    downloads_low = daily_searches * ttr * conv_low
    downloads_high = daily_searches * ttr * conv_high

    return {
        "position": position,
        "estimated_daily_searches": round(daily_searches),
        "tap_through_rate": round(ttr, 4),
        "downloads_per_day_low": round(downloads_low, 1),
        "downloads_per_day_high": round(downloads_high, 1),
    }


def score_keyword(result: SearchResult) -> KeywordScore:
    """Run full scoring pipeline on a search result."""
    pop = score_popularity(result)
    diff = score_difficulty(result)

    # Opportunity = popularity weighted against inverse difficulty
    # High pop + low diff = high opportunity
    diff_factor = 1 - (diff.total / 100)
    opp = pop.total * diff_factor
    # Normalise to 1-100
    opportunity = max(1.0, min(100.0, opp * 1.2))

    label = _opportunity_label(pop.total, diff.total)

    top_apps = [a.to_dict() for a in result.apps[:10]]

    # Download estimates for positions 1, 3, 5, 10
    dl_estimates = {
        f"position_{p}": estimate_downloads(pop.total, p)
        for p in [1, 3, 5, 10]
    }

    return KeywordScore(
        keyword=result.keyword,
        country=result.country,
        popularity=pop,
        difficulty=diff,
        opportunity=opportunity,
        opportunity_label=label,
        result_count=result.result_count,
        top_apps=top_apps,
        download_estimates=dl_estimates,
    )
