"""ASO MCP Server — AI-first App Store keyword research via iTunes Search API.

Tools:
  - aso_research_keyword: Full keyword analysis with popularity, difficulty, opportunity scores
  - aso_batch_research: Research multiple keywords at once and rank by opportunity
  - aso_competitor_analysis: Analyse top apps for a keyword
  - aso_find_opportunities: Scan a list of keywords and return only good opportunities
  - aso_suggest_metadata: Generate App Store title, subtitle, and keyword field suggestions
  - aso_country_scan: Check a keyword across multiple App Store regions
"""

import json
from contextlib import asynccontextmanager
from typing import Optional

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field, ConfigDict, field_validator

from .itunes import ITunesClient, SearchResult
from .scoring import score_keyword, KeywordScore, estimate_downloads


# --- Lifespan: shared iTunes client ---

@asynccontextmanager
async def app_lifespan(server):
    client = ITunesClient()
    yield {"itunes": client}
    await client.close()


mcp = FastMCP("aso_mcp", lifespan=app_lifespan)


# --- Helpers ---

def _get_client(ctx) -> ITunesClient:
    return ctx.request_context.lifespan_state["itunes"]


def _format_score_markdown(score: KeywordScore) -> str:
    """Format a keyword score as readable markdown."""
    pop = score.popularity
    diff = score.difficulty

    lines = [
        f"## {score.keyword} ({score.country})",
        "",
        f"**Opportunity: {round(score.opportunity)}/100 — {score.opportunity_label}**",
        "",
        f"| Metric | Score |",
        f"|--------|-------|",
        f"| Popularity | {round(pop.total)}/100 |",
        f"| Difficulty | {round(diff.total)}/100 |",
        f"| App Store results | {score.result_count} |",
        "",
        "### Popularity breakdown",
        f"- Result count signal: {round(pop.result_count_score, 1)}/25",
        f"- Leader strength: {round(pop.leader_strength_score, 1)}/30",
        f"- Title match density: {round(pop.title_match_score, 1)}/20",
        f"- Market depth: {round(pop.market_depth_score, 1)}/10",
        f"- Specificity penalty: {round(pop.specificity_penalty, 1)}",
        f"- Exact phrase bonus: {round(pop.exact_phrase_bonus, 1)}/15",
        "",
        "### Difficulty breakdown",
        f"- Rating volume (30%): {round(diff.rating_volume_score, 1)}/100",
        f"- Dominant players (20%): {round(diff.dominant_players_score, 1)}/100",
        f"- Rating quality (10%): {round(diff.rating_quality_score, 1)}/100",
        f"- Market maturity (10%): {round(diff.market_maturity_score, 1)}/100",
        f"- Publisher diversity (10%): {round(diff.publisher_diversity_score, 1)}/100",
        f"- App count (10%): {round(diff.app_count_score, 1)}/100",
        f"- Content relevance (10%): {round(diff.content_relevance_score, 1)}/100",
        "",
        "### Download estimates (free app)",
    ]
    for key, dl in score.download_estimates.items():
        lines.append(
            f"- Position #{dl['position']}: {dl['downloads_per_day_low']}-{dl['downloads_per_day_high']} downloads/day"
        )

    if score.top_apps:
        lines.append("")
        lines.append("### Top competing apps")
        for i, app in enumerate(score.top_apps[:5], 1):
            lines.append(
                f"{i}. **{app['name']}** by {app['developer']} — "
                f"{app['rating']}★ ({app['rating_count']:,} ratings) "
                f"{'Free' if app['price'] == 0 else 'Paid'}"
            )

    return "\n".join(lines)


# --- Input models ---

class ResearchKeywordInput(BaseModel):
    """Input for single keyword research."""
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    keyword: str = Field(..., description="The App Store keyword to research (e.g. 'cable identifier', 'habit tracker')", min_length=1, max_length=100)
    country: str = Field(default="US", description="2-letter country code for the App Store region (e.g. US, GB, DE, JP)", min_length=2, max_length=2)

    @field_validator("country")
    @classmethod
    def uppercase_country(cls, v: str) -> str:
        return v.upper()


class BatchResearchInput(BaseModel):
    """Input for batch keyword research."""
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    keywords: list[str] = Field(..., description="List of keywords to research (max 20)", min_length=1, max_length=20)
    country: str = Field(default="US", description="2-letter country code", min_length=2, max_length=2)

    @field_validator("country")
    @classmethod
    def uppercase_country(cls, v: str) -> str:
        return v.upper()


class CompetitorInput(BaseModel):
    """Input for competitor analysis."""
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    keyword: str = Field(..., description="Keyword to analyze competitors for", min_length=1, max_length=100)
    country: str = Field(default="US", description="2-letter country code", min_length=2, max_length=2)
    top_n: int = Field(default=10, description="Number of top apps to return", ge=1, le=50)

    @field_validator("country")
    @classmethod
    def uppercase_country(cls, v: str) -> str:
        return v.upper()


class OpportunityInput(BaseModel):
    """Input for opportunity scanning."""
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    keywords: list[str] = Field(..., description="Keywords to scan for opportunities (max 30)", min_length=1, max_length=30)
    country: str = Field(default="US", description="2-letter country code", min_length=2, max_length=2)
    min_popularity: int = Field(default=20, description="Minimum popularity score to include (1-100)", ge=1, le=100)
    max_difficulty: int = Field(default=50, description="Maximum difficulty score to include (1-100)", ge=1, le=100)

    @field_validator("country")
    @classmethod
    def uppercase_country(cls, v: str) -> str:
        return v.upper()


class MetadataInput(BaseModel):
    """Input for metadata suggestions."""
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    app_name: str = Field(..., description="Your app's name", min_length=1, max_length=50)
    primary_keyword: str = Field(..., description="Main keyword you want to rank for", min_length=1, max_length=100)
    secondary_keywords: list[str] = Field(default_factory=list, description="Additional keywords to incorporate (max 10)", max_length=10)
    country: str = Field(default="US", description="2-letter country code", min_length=2, max_length=2)

    @field_validator("country")
    @classmethod
    def uppercase_country(cls, v: str) -> str:
        return v.upper()


class CountryScanInput(BaseModel):
    """Input for cross-country keyword scanning."""
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    keyword: str = Field(..., description="Keyword to scan across countries", min_length=1, max_length=100)
    countries: list[str] = Field(
        default=["US", "GB", "CA", "AU", "DE", "FR", "JP", "KR", "BR", "IN"],
        description="Country codes to scan (max 15)",
        max_length=15,
    )


# --- Tools ---

@mcp.tool(
    name="aso_research_keyword",
    annotations={
        "title": "Research App Store Keyword",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def research_keyword(params: ResearchKeywordInput, ctx=None) -> str:
    """Research a single App Store keyword with full scoring.

    Returns popularity score (1-100), difficulty score (1-100), opportunity score,
    download estimates per ranking position, and top competing apps.

    Args:
        params: keyword and country code

    Returns:
        Markdown-formatted keyword analysis with scores and competitor data
    """
    client = _get_client(ctx)
    result = await client.search(params.keyword, params.country, limit=200)
    score = score_keyword(result)
    return _format_score_markdown(score)


@mcp.tool(
    name="aso_batch_research",
    annotations={
        "title": "Batch Research Keywords",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def batch_research(params: BatchResearchInput, ctx=None) -> str:
    """Research multiple keywords and rank them by opportunity score.

    Returns a comparison table sorted by opportunity (best first),
    plus detailed breakdown for each keyword.

    Args:
        params: list of keywords and country code

    Returns:
        Markdown table ranking keywords by opportunity, with full details
    """
    client = _get_client(ctx)
    scores: list[KeywordScore] = []

    for kw in params.keywords:
        try:
            result = await client.search(kw, params.country, limit=200)
            scores.append(score_keyword(result))
        except Exception as e:
            scores.append(None)

    # Sort by opportunity descending
    valid = [(s, kw) for s, kw in zip(scores, params.keywords) if s is not None]
    valid.sort(key=lambda x: x[0].opportunity, reverse=True)

    lines = [
        f"# Keyword Research — {params.country} App Store",
        f"Researched {len(params.keywords)} keywords",
        "",
        "| Rank | Keyword | Popularity | Difficulty | Opportunity | Label | Results |",
        "|------|---------|-----------|------------|-------------|-------|---------|",
    ]

    for i, (s, kw) in enumerate(valid, 1):
        lines.append(
            f"| {i} | {s.keyword} | {round(s.popularity.total)} | "
            f"{round(s.difficulty.total)} | **{round(s.opportunity)}** | "
            f"{s.opportunity_label} | {s.result_count} |"
        )

    failed = [kw for s, kw in zip(scores, params.keywords) if s is None]
    if failed:
        lines.append(f"\n*Failed to research: {', '.join(failed)}*")

    lines.append("\n---\n")
    for s, kw in valid:
        lines.append(_format_score_markdown(s))
        lines.append("\n---\n")

    return "\n".join(lines)


@mcp.tool(
    name="aso_competitor_analysis",
    annotations={
        "title": "Analyze Competitors for Keyword",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def competitor_analysis(params: CompetitorInput, ctx=None) -> str:
    """Deep competitor analysis for a keyword — shows top ranking apps with details.

    Returns app names, ratings, review counts, pricing, genres, developer info,
    and age for each competing app.

    Args:
        params: keyword, country, and number of top apps to analyse

    Returns:
        Markdown competitor breakdown with strategic insights
    """
    client = _get_client(ctx)
    result = await client.search(params.keyword, params.country, limit=200)

    apps = result.apps[:params.top_n]
    keyword = params.keyword.lower()

    lines = [
        f"# Competitor Analysis: \"{params.keyword}\" ({params.country})",
        f"Total results: {result.result_count}",
        "",
    ]

    free_count = sum(1 for a in apps if a.price == 0)
    avg_rating = sum(a.rating for a in apps if a.rating > 0) / max(1, sum(1 for a in apps if a.rating > 0))
    keyword_in_title = sum(1 for a in apps if keyword in a.name.lower())

    lines.extend([
        "### Market overview",
        f"- Free apps in top {len(apps)}: {free_count}/{len(apps)}",
        f"- Average rating: {avg_rating:.1f}★",
        f"- Apps with keyword in title: {keyword_in_title}/{len(apps)}",
        "",
        "### Top apps",
        "",
    ])

    for i, app in enumerate(apps, 1):
        from .scoring import _app_age_days
        age = _app_age_days(app)
        age_str = f"{age} days" if age is not None else "unknown"

        lines.extend([
            f"**{i}. {app.name}**",
            f"- Developer: {app.developer}",
            f"- Rating: {app.rating:.1f}★ ({app.rating_count:,} ratings)",
            f"- Price: {'Free' if app.price == 0 else f'${app.price}'}",
            f"- Genre: {app.primary_genre}",
            f"- Age: {age_str}",
            f"- Content rating: {app.content_rating}",
            f"- [App Store link]({app.store_url})" if app.store_url else "",
            "",
        ])

    return "\n".join(lines)


@mcp.tool(
    name="aso_find_opportunities",
    annotations={
        "title": "Find Keyword Opportunities",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def find_opportunities(params: OpportunityInput, ctx=None) -> str:
    """Scan keywords and return only those meeting opportunity thresholds.

    Filters for keywords with popularity >= min_popularity AND
    difficulty <= max_difficulty. Returns them ranked by opportunity score.

    Args:
        params: keywords, country, min popularity, max difficulty thresholds

    Returns:
        Filtered and ranked opportunity list with scores
    """
    client = _get_client(ctx)
    all_scores: list[KeywordScore] = []
    errors: list[str] = []

    for kw in params.keywords:
        try:
            result = await client.search(kw, params.country, limit=200)
            all_scores.append(score_keyword(result))
        except Exception as e:
            errors.append(f"{kw}: {e}")

    # Filter
    opportunities = [
        s for s in all_scores
        if s.popularity.total >= params.min_popularity
        and s.difficulty.total <= params.max_difficulty
    ]
    opportunities.sort(key=lambda s: s.opportunity, reverse=True)

    rejected = [
        s for s in all_scores
        if s not in opportunities
    ]

    lines = [
        f"# Opportunity Scan — {params.country} App Store",
        f"Scanned {len(params.keywords)} keywords | "
        f"Filter: popularity ≥ {params.min_popularity}, difficulty ≤ {params.max_difficulty}",
        f"**Found {len(opportunities)} opportunities**",
        "",
    ]

    if opportunities:
        lines.extend([
            "| Rank | Keyword | Pop | Diff | Opportunity | Label |",
            "|------|---------|-----|------|-------------|-------|",
        ])
        for i, s in enumerate(opportunities, 1):
            lines.append(
                f"| {i} | {s.keyword} | {round(s.popularity.total)} | "
                f"{round(s.difficulty.total)} | **{round(s.opportunity)}** | {s.opportunity_label} |"
            )

        lines.append("\n### Details\n")
        for s in opportunities:
            lines.append(_format_score_markdown(s))
            lines.append("\n---\n")

    if rejected:
        lines.append("\n### Rejected keywords\n")
        for s in rejected:
            reason = []
            if s.popularity.total < params.min_popularity:
                reason.append(f"popularity {round(s.popularity.total)} < {params.min_popularity}")
            if s.difficulty.total > params.max_difficulty:
                reason.append(f"difficulty {round(s.difficulty.total)} > {params.max_difficulty}")
            lines.append(f"- ~~{s.keyword}~~ — {', '.join(reason)}")

    if errors:
        lines.append(f"\n*Errors: {'; '.join(errors)}*")

    return "\n".join(lines)


@mcp.tool(
    name="aso_suggest_metadata",
    annotations={
        "title": "Suggest App Store Metadata",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def suggest_metadata(params: MetadataInput, ctx=None) -> str:
    """Generate App Store metadata suggestions based on keyword research.

    Researches the primary keyword and all secondary keywords, then suggests
    optimised title (30 chars), subtitle (30 chars), and keyword field (100 chars).

    Args:
        params: app name, primary keyword, secondary keywords, country

    Returns:
        Metadata suggestions with keyword data backing each recommendation
    """
    client = _get_client(ctx)

    all_keywords = [params.primary_keyword] + list(params.secondary_keywords)
    scores: list[KeywordScore] = []

    for kw in all_keywords:
        try:
            result = await client.search(kw, params.country, limit=200)
            scores.append(score_keyword(result))
        except Exception:
            pass

    scores.sort(key=lambda s: s.opportunity, reverse=True)

    # Build keyword field from highest-opportunity keywords
    # Exclude words already in app name or primary keyword
    name_words = set(params.app_name.lower().split())
    used_words = set()
    keyword_field_parts = []

    for s in scores:
        words = s.keyword.lower().split()
        new_words = [w for w in words if w not in name_words and w not in used_words]
        if new_words:
            keyword_field_parts.append(",".join(new_words))
            used_words.update(new_words)

    keyword_field = ",".join(keyword_field_parts)[:100]

    lines = [
        f"# Metadata Suggestions for {params.app_name}",
        "",
        "### Keyword data",
        "",
        "| Keyword | Pop | Diff | Opportunity |",
        "|---------|-----|------|-------------|",
    ]

    for s in scores:
        lines.append(
            f"| {s.keyword} | {round(s.popularity.total)} | "
            f"{round(s.difficulty.total)} | {round(s.opportunity)} |"
        )

    best = scores[0] if scores else None
    second = scores[1] if len(scores) > 1 else None

    title_suggestion = f"{params.app_name}"
    if best:
        remaining = 30 - len(title_suggestion) - 3  # " - " separator
        if remaining > 3:
            title_suggestion += f" - {best.keyword[:remaining]}"

    subtitle_suggestion = ""
    if second:
        subtitle_suggestion = second.keyword[:30]
    elif best:
        subtitle_suggestion = best.keyword[:30]

    lines.extend([
        "",
        "### Suggested metadata",
        "",
        f"**Title** (≤30 chars): `{title_suggestion[:30]}`",
        f"  Characters used: {len(title_suggestion[:30])}/30",
        "",
        f"**Subtitle** (≤30 chars): `{subtitle_suggestion[:30]}`",
        f"  Characters used: {len(subtitle_suggestion[:30])}/30",
        "",
        f"**Keyword field** (≤100 chars):",
        f"```",
        f"{keyword_field}",
        f"```",
        f"  Characters used: {len(keyword_field)}/100",
        "",
        "### Notes",
        "- Apple does NOT index words already in your title/subtitle in the keyword field — don't repeat them",
        "- Separate keywords with commas, no spaces",
        "- Use singular forms (Apple indexes both singular and plural)",
        "- Don't include 'app' or your category name (already indexed)",
    ])

    return "\n".join(lines)


@mcp.tool(
    name="aso_country_scan",
    annotations={
        "title": "Scan Keyword Across Countries",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def country_scan(params: CountryScanInput, ctx=None) -> str:
    """Scan a keyword across multiple App Store countries to find the best regions.

    Useful for finding countries where competition is lower for the same keyword.

    Args:
        params: keyword and list of country codes

    Returns:
        Country comparison table ranked by opportunity
    """
    client = _get_client(ctx)
    results: list[tuple[str, KeywordScore]] = []
    errors: list[str] = []

    for country in params.countries:
        try:
            result = await client.search(params.keyword, country.upper(), limit=200)
            score = score_keyword(result)
            results.append((country.upper(), score))
        except Exception as e:
            errors.append(f"{country}: {e}")

    results.sort(key=lambda x: x[1].opportunity, reverse=True)

    lines = [
        f"# Country Scan: \"{params.keyword}\"",
        f"Scanned {len(params.countries)} countries",
        "",
        "| Rank | Country | Pop | Diff | Opportunity | Label | Results |",
        "|------|---------|-----|------|-------------|-------|---------|",
    ]

    for i, (country, s) in enumerate(results, 1):
        lines.append(
            f"| {i} | {country} | {round(s.popularity.total)} | "
            f"{round(s.difficulty.total)} | **{round(s.opportunity)}** | "
            f"{s.opportunity_label} | {s.result_count} |"
        )

    if errors:
        lines.append(f"\n*Errors: {'; '.join(errors)}*")

    return "\n".join(lines)


# --- Entry point ---

def main():
    mcp.run()


if __name__ == "__main__":
    main()
